mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Batch updating and deleting, multi-select
This commit is contained in:
		
							parent
							
								
									8c9fb0d45e
								
							
						
					
					
						commit
						88c7c1632e
					
				| @ -75,6 +75,10 @@ | ||||
|   box-shadow: 2px 8px 6px #111111aa; | ||||
| } | ||||
| 
 | ||||
| .box-shadow-lg-up { | ||||
|   box-shadow: 0px -12px 8px #111111ee; | ||||
| } | ||||
| 
 | ||||
| .box-shadow-xl { | ||||
|   box-shadow: 2px 14px 8px #111111aa; | ||||
| } | ||||
| @ -17,6 +17,16 @@ | ||||
| 
 | ||||
|         <ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" /> | ||||
|       </div> | ||||
| 
 | ||||
|       <div v-show="numAudiobooksSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center"> | ||||
|         <h1 class="text-2xl px-4">{{ numAudiobooksSelected }} Selected</h1> | ||||
|         <ui-btn small class="text-sm mx-2" @click="toggleSelectAll">{{ isAllSelected ? 'Select None' : 'Select All' }}</ui-btn> | ||||
| 
 | ||||
|         <div class="flex-grow" /> | ||||
|         <ui-btn v-show="!processingBatchDelete" color="warning" small class="mx-2" @click="batchEditClick"><span class="material-icons text-gray-200 pt-1">edit</span></ui-btn> | ||||
|         <ui-btn color="error" small class="mx-2" :loading="processingBatchDelete" @click="batchDeleteClick"><span class="material-icons text-gray-200 pt-1">delete</span></ui-btn> | ||||
|         <span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @ -35,7 +45,8 @@ export default { | ||||
|           value: 'logout', | ||||
|           text: 'Logout' | ||||
|         } | ||||
|       ] | ||||
|       ], | ||||
|       processingBatchDelete: false | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -47,6 +58,18 @@ export default { | ||||
|     }, | ||||
|     username() { | ||||
|       return this.user ? this.user.username : 'err' | ||||
|     }, | ||||
|     numAudiobooksSelected() { | ||||
|       return this.selectedAudiobooks.length | ||||
|     }, | ||||
|     selectedAudiobooks() { | ||||
|       return this.$store.state.selectedAudiobooks | ||||
|     }, | ||||
|     isAllSelected() { | ||||
|       return this.audiobooksShowing.length === this.selectedAudiobooks.length | ||||
|     }, | ||||
|     audiobooksShowing() { | ||||
|       return this.$store.getters['audiobooks/getFiltered']() | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
| @ -57,10 +80,6 @@ export default { | ||||
|         this.$router.push('/') | ||||
|       } | ||||
|     }, | ||||
|     scan() { | ||||
|       console.log('Call Start Init') | ||||
|       this.$root.socket.emit('scan') | ||||
|     }, | ||||
|     logout() { | ||||
|       this.$axios.$post('/logout').catch((error) => { | ||||
|         console.error(error) | ||||
| @ -74,6 +93,43 @@ export default { | ||||
|       if (action === 'logout') { | ||||
|         this.logout() | ||||
|       } | ||||
|     }, | ||||
|     cancelSelectionMode() { | ||||
|       if (this.processingBatchDelete) return | ||||
|       this.$store.commit('setSelectedAudiobooks', []) | ||||
|     }, | ||||
|     toggleSelectAll() { | ||||
|       if (this.isAllSelected) { | ||||
|         this.cancelSelectionMode() | ||||
|       } else { | ||||
|         var audiobookIds = this.audiobooksShowing.map((a) => a.id) | ||||
|         this.$store.commit('setSelectedAudiobooks', audiobookIds) | ||||
|       } | ||||
|     }, | ||||
|     batchDeleteClick() { | ||||
|       if (confirm(`Are you sure you want to delete these ${this.numAudiobooksSelected} audiobook(s)?`)) { | ||||
|         this.processingBatchDelete = true | ||||
|         this.$store.commit('setProcessingBatch', true) | ||||
|         this.$axios | ||||
|           .$post(`/api/audiobooks/delete`, { | ||||
|             audiobookIds: this.selectedAudiobooks | ||||
|           }) | ||||
|           .then(() => { | ||||
|             this.$toast.success('Batch delete success!') | ||||
|             this.processingBatchDelete = false | ||||
|             this.$store.commit('setProcessingBatch', false) | ||||
|             this.$store.commit('setSelectedAudiobooks', []) | ||||
|           }) | ||||
|           .catch((error) => { | ||||
|             this.$toast.error('Batch delete failed') | ||||
|             console.error('Failed to batch delete', error) | ||||
|             this.processingBatchDelete = false | ||||
|             this.$store.commit('setProcessingBatch', false) | ||||
|           }) | ||||
|       } | ||||
|     }, | ||||
|     batchEditClick() { | ||||
|       this.$router.push('/batch') | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <template> | ||||
|   <div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-auto relative"> | ||||
|     <!-- Cover size widget --> | ||||
|     <div class="fixed bottom-2 right-4 z-20"> | ||||
|     <div v-show="!isSelectionMode" class="fixed bottom-2 right-4 z-20"> | ||||
|       <div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent> | ||||
|         <span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize">remove</span> | ||||
|         <p class="px-2 font-mono">{{ bookCoverWidth }}</p> | ||||
| @ -62,6 +62,9 @@ export default { | ||||
|     }, | ||||
|     bookWidth() { | ||||
|       return this.bookCoverWidth + this.paddingX * 2 | ||||
|     }, | ||||
|     isSelectionMode() { | ||||
|       return this.$store.getters['getNumAudiobooksSelected'] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
| @ -102,8 +105,6 @@ export default { | ||||
|       this.width = Math.max(0, this.width - this.rowPaddingX * 2) | ||||
|       var booksPerRow = Math.floor(this.width / this.bookWidth) | ||||
|       this.booksPerRow = booksPerRow | ||||
|       console.warn('this.selectedSizeIndex', this.selectedSizeIndex, 'Book Cover Size', this.bookCoverWidth) | ||||
|       console.warn('Books Per Row', this.booksPerRow, 'Width', this.width, 'Book Width', this.bookWidth) | ||||
|     }, | ||||
|     getAudiobookCard(id) { | ||||
|       if (this.$refs[`audiobookCard-${id}`] && this.$refs[`audiobookCard-${id}`].length) { | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-40 z-20 bg-primary p-4"> | ||||
|   <div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-40 z-40 bg-primary p-4"> | ||||
|     <div class="absolute -top-16 left-4"> | ||||
|       <cards-book-cover :audiobook="streamAudiobook" :width="88" /> | ||||
|     </div> | ||||
|  | ||||
| @ -1,28 +1,32 @@ | ||||
| <template> | ||||
|   <nuxt-link :to="`/audiobook/${audiobookId}`" :style="{ padding: `16px ${paddingX}px` }" class="cursor-pointer"> | ||||
|     <div class="rounded-sm h-full overflow-hidden relative bookCard" @mouseover="isHovering = true" @mouseleave="isHovering = false"> | ||||
|   <div class="rounded-sm h-full overflow-hidden relative bookCard" :style="{ padding: `16px ${paddingX}px` }" @mouseover="isHovering = true" @mouseleave="isHovering = false" @click="clickCard"> | ||||
|     <nuxt-link :to="isSelectionMode ? '' : `/audiobook/${audiobookId}`" class="cursor-pointer"> | ||||
|       <div class="w-full relative" :style="{ height: height + 'px' }"> | ||||
|         <cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" /> | ||||
| 
 | ||||
|         <div v-show="isHovering" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40"> | ||||
|           <div class="h-full flex items-center justify-center"> | ||||
|         <div v-show="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black rounded" :class="overlayWrapperClasslist"> | ||||
|           <div v-show="!isSelectionMode" 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 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-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> | ||||
|           </div> | ||||
|           <div class="absolute cursor-pointer hover:text-yellow-300 hover:scale-125 transform duration-100" :style="{ top: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="selectBtnClick"> | ||||
|             <span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="absolute bottom-0 left-0 h-1 bg-yellow-400 shadow-sm" :style="{ width: width * userProgressPercent + 'px' }"></div> | ||||
|         <div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 bg-yellow-400 shadow-sm" :style="{ width: width * userProgressPercent + 'px' }"></div> | ||||
| 
 | ||||
|         <ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0"> | ||||
|           <div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300"> | ||||
|             <span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span> | ||||
|           </div> | ||||
|         </ui-tooltip> | ||||
|       </div> | ||||
|       <ui-tooltip v-if="showError" :text="errorText" class="absolute top-4 left-0"> | ||||
|         <div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300"> | ||||
|           <span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span> | ||||
|         </div> | ||||
|       </ui-tooltip> | ||||
|     </div> | ||||
|   </nuxt-link> | ||||
|     </nuxt-link> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| @ -50,6 +54,18 @@ export default { | ||||
|     audiobookId() { | ||||
|       return this.audiobook.id | ||||
|     }, | ||||
|     isSelectionMode() { | ||||
|       return this.$store.getters['getNumAudiobooksSelected'] | ||||
|     }, | ||||
|     selectedAudiobooks() { | ||||
|       return this.$store.state.selectedAudiobooks | ||||
|     }, | ||||
|     selected() { | ||||
|       return this.$store.getters['getIsAudiobookSelected'](this.audiobookId) | ||||
|     }, | ||||
|     processingBatch() { | ||||
|       return this.$store.state.processingBatch | ||||
|     }, | ||||
|     book() { | ||||
|       return this.audiobook.book || {} | ||||
|     }, | ||||
| @ -112,9 +128,22 @@ export default { | ||||
|         txt += `${this.hasInvalidParts} invalid parts.` | ||||
|       } | ||||
|       return txt || 'Unknown Error' | ||||
|     }, | ||||
|     overlayWrapperClasslist() { | ||||
|       var classes = [] | ||||
|       if (this.isSelectionMode) classes.push('bg-opacity-60') | ||||
|       else classes.push('bg-opacity-40') | ||||
|       if (this.selected) { | ||||
|         classes.push('border-2 border-yellow-400') | ||||
|       } | ||||
|       return classes | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     selectBtnClick() { | ||||
|       if (this.processingBatch) return | ||||
|       this.$store.commit('toggleAudiobookSelected', this.audiobookId) | ||||
|     }, | ||||
|     clickError(e) { | ||||
|       e.stopPropagation() | ||||
|       this.$router.push(`/audiobook/${this.audiobookId}`) | ||||
| @ -125,6 +154,13 @@ export default { | ||||
|     }, | ||||
|     editClick() { | ||||
|       this.$store.commit('showEditModal', this.audiobook) | ||||
|     }, | ||||
|     clickCard(e) { | ||||
|       if (this.isSelectionMode) { | ||||
|         e.stopPropagation() | ||||
|         e.preventDefault() | ||||
|         this.selectBtnClick() | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
|  | ||||
| @ -91,12 +91,20 @@ export default { | ||||
|         } | ||||
|         this.isFocused = false | ||||
|         if (this.input !== this.textInput) { | ||||
|           this.input = this.$cleanString(this.textInput) || null | ||||
|           var val = this.$cleanString(this.textInput) || null | ||||
|           this.input = val | ||||
|           if (val && !this.items.includes(val)) { | ||||
|             this.$emit('newItem', val) | ||||
|           } | ||||
|         } | ||||
|       }, 50) | ||||
|     }, | ||||
|     submitForm() { | ||||
|       this.input = this.$cleanString(this.textInput) || null | ||||
|       var val = this.$cleanString(this.textInput) || null | ||||
|       this.input = val | ||||
|       if (val && !this.items.includes(val)) { | ||||
|         this.$emit('newItem', val) | ||||
|       } | ||||
|       this.currentSearch = null | ||||
|     }, | ||||
|     clickedOption(e, item) { | ||||
|  | ||||
| @ -21,6 +21,9 @@ export default { | ||||
|       if (this.$store.state.showEditModal) { | ||||
|         this.$store.commit('setShowEditModal', false) | ||||
|       } | ||||
|       if (this.$store.state.selectedAudiobooks) { | ||||
|         this.$store.commit('setSelectedAudiobooks', []) | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| export default function ({ store, redirect, route }) { | ||||
|   // If the user is not authenticated
 | ||||
|   if (!store.state.user.user) { | ||||
|     if (route.name === 'batch') return redirect('/login') | ||||
|     return redirect(`/login?redirect=${route.path}`) | ||||
|   } | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "0.9.83-beta", | ||||
|   "version": "0.9.84-beta", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
							
								
								
									
										142
									
								
								client/pages/batch/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								client/pages/batch/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,142 @@ | ||||
| <template> | ||||
|   <div ref="page" id="page-wrapper" class="page px-6 pt-6 pb-52 overflow-y-auto" :class="streamAudiobook ? 'streaming' : ''"> | ||||
|     <div class="flex justify-center flex-wrap"> | ||||
|       <template v-for="audiobook in audiobookCopies"> | ||||
|         <div :key="audiobook.id" class="w-full max-w-3xl border border-black-300 p-6 -ml-px -mt-px flex"> | ||||
|           <div class="w-32"> | ||||
|             <cards-book-cover :audiobook="audiobook.originalAudiobook" :width="120" /> | ||||
|           </div> | ||||
|           <div class="flex-grow pl-4"> | ||||
|             <ui-text-input-with-label v-model="audiobook.book.title" label="Title" /> | ||||
| 
 | ||||
|             <div class="flex mt-2 -mx-1"> | ||||
|               <div class="w-3/4 px-1"> | ||||
|                 <ui-text-input-with-label v-model="audiobook.book.author" label="Author" /> | ||||
|               </div> | ||||
|               <div class="flex-grow px-1"> | ||||
|                 <ui-text-input-with-label v-model="audiobook.book.publishYear" type="number" label="Publish Year" /> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="flex mt-2 -mx-1"> | ||||
|               <div class="w-3/4 px-1"> | ||||
|                 <ui-input-dropdown v-model="audiobook.book.series" label="Series" :items="seriesItems" @input="seriesChanged" @newItem="newSeriesItem" /> | ||||
|               </div> | ||||
|               <div class="flex-grow px-1"> | ||||
|                 <ui-text-input-with-label v-model="audiobook.book.volumeNumber" label="Volume #" /> | ||||
|               </div> | ||||
|             </div> | ||||
| 
 | ||||
|             <ui-textarea-with-label v-model="audiobook.book.description" :rows="3" label="Description" class="mt-2" /> | ||||
| 
 | ||||
|             <div class="flex mt-2 -mx-1"> | ||||
|               <div class="w-1/2 px-1"> | ||||
|                 <ui-multi-select v-model="audiobook.book.genres" label="Genres" :items="genres" /> | ||||
|               </div> | ||||
|               <div class="flex-grow px-1"> | ||||
|                 <ui-multi-select v-model="audiobook.tags" label="Tags" :items="tags" /> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </template> | ||||
|     </div> | ||||
|     <div v-show="isProcessing" class="fixed top-0 left-0 z-50 w-full h-full flex items-center justify-center bg-black bg-opacity-60"> | ||||
|       <ui-loading-indicator /> | ||||
|     </div> | ||||
| 
 | ||||
|     <div :class="isScrollable ? 'fixed left-0 box-shadow-lg-up bg-primary' : ''" class="w-full h-20 px-4 flex items-center border-t border-bg z-40" :style="{ bottom: streamAudiobook ? '165px' : '0px' }"> | ||||
|       <div class="flex-grow" /> | ||||
|       <ui-btn color="success" :padding-x="8" class="text-lg" :loading="isProcessing" @click="saveClick">Save</ui-btn> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   asyncData({ store, redirect }) { | ||||
|     if (!store.state.selectedAudiobooks.length) { | ||||
|       return redirect('/') | ||||
|     } | ||||
|     var audiobooks = store.state.audiobooks.audiobooks.filter((ab) => store.state.selectedAudiobooks.includes(ab.id)) | ||||
|     return { | ||||
|       audiobooks | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       isProcessing: false, | ||||
|       audiobookCopies: [], | ||||
|       isScrollable: false, | ||||
|       newSeriesItems: [] | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     streamAudiobook() { | ||||
|       return this.$store.state.streamAudiobook | ||||
|     }, | ||||
|     genres() { | ||||
|       return this.$store.state.audiobooks.genres | ||||
|     }, | ||||
|     tags() { | ||||
|       return this.$store.state.audiobooks.tags | ||||
|     }, | ||||
|     series() { | ||||
|       return this.$store.state.audiobooks.series | ||||
|     }, | ||||
|     seriesItems() { | ||||
|       return [...this.series, ...this.newSeriesItems] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     newSeriesItem(item) { | ||||
|       if (!item) return | ||||
|       this.newSeriesItems.push(item) | ||||
|     }, | ||||
|     seriesChanged() { | ||||
|       this.newSeriesItems = this.newSeriesItems.filter((item) => { | ||||
|         return this.audiobookCopies.find((ab) => ab.book.series === item) | ||||
|       }) | ||||
|     }, | ||||
|     init() { | ||||
|       this.audiobookCopies = this.audiobooks.map((ab) => { | ||||
|         var copy = { ...ab } | ||||
|         copy.tags = [...ab.tags] | ||||
|         copy.book = { ...ab.book } | ||||
|         copy.book.genres = [...ab.book.genres] | ||||
|         copy.originalAudiobook = ab | ||||
|         return copy | ||||
|       }) | ||||
|       this.$nextTick(() => { | ||||
|         if (this.$refs.page.scrollHeight > this.$refs.page.clientHeight) { | ||||
|           this.isScrollable = true | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     saveClick() { | ||||
|       this.isProcessing = true | ||||
| 
 | ||||
|       this.$axios | ||||
|         .$post('/api/audiobooks/update', this.audiobookCopies) | ||||
|         .then((data) => { | ||||
|           this.isProcessing = false | ||||
|           if (data.updates) { | ||||
|             this.$toast.success(`Successfully updated ${data.updates} audiobooks`) | ||||
|             this.$router.replace('/') | ||||
|           } else { | ||||
|             this.$toast.warning('No updates were necessary') | ||||
|           } | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|           console.error('failed to batch update', error) | ||||
|           this.$toast.error('Failed to batch update') | ||||
|           this.isProcessing = false | ||||
|         }) | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.init() | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| @ -1,3 +1,4 @@ | ||||
| import Vue from 'vue' | ||||
| 
 | ||||
| export const state = () => ({ | ||||
|   streamAudiobook: null, | ||||
| @ -8,10 +9,17 @@ export const state = () => ({ | ||||
|   isScanningCovers: false, | ||||
|   scanProgress: null, | ||||
|   coverScanProgress: null, | ||||
|   developerMode: false | ||||
|   developerMode: false, | ||||
|   selectedAudiobooks: [], | ||||
|   processingBatch: false | ||||
| }) | ||||
| 
 | ||||
| export const getters = {} | ||||
| export const getters = { | ||||
|   getIsAudiobookSelected: state => audiobookId => { | ||||
|     return !!state.selectedAudiobooks.includes(audiobookId) | ||||
|   }, | ||||
|   getNumAudiobooksSelected: state => state.selectedAudiobooks.length | ||||
| } | ||||
| 
 | ||||
| export const actions = {} | ||||
| 
 | ||||
| @ -56,5 +64,18 @@ export const mutations = { | ||||
|   }, | ||||
|   setDeveloperMode(state, val) { | ||||
|     state.developerMode = val | ||||
|   }, | ||||
|   setSelectedAudiobooks(state, audiobooks) { | ||||
|     state.selectedAudiobooks = audiobooks | ||||
|   }, | ||||
|   toggleAudiobookSelected(state, audiobookId) { | ||||
|     if (state.selectedAudiobooks.includes(audiobookId)) { | ||||
|       state.selectedAudiobooks = state.selectedAudiobooks.filter(a => a !== audiobookId) | ||||
|     } else { | ||||
|       state.selectedAudiobooks.push(audiobookId) | ||||
|     } | ||||
|   }, | ||||
|   setProcessingBatch(state, val) { | ||||
|     state.processingBatch = val | ||||
|   } | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "0.9.83-beta", | ||||
|   "version": "0.9.84-beta", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks.", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -21,6 +21,8 @@ class ApiController { | ||||
| 
 | ||||
|     this.router.get('/audiobooks', this.getAudiobooks.bind(this)) | ||||
|     this.router.delete('/audiobooks', this.deleteAllAudiobooks.bind(this)) | ||||
|     this.router.post('/audiobooks/delete', this.batchDeleteAudiobooks.bind(this)) | ||||
|     this.router.post('/audiobooks/update', this.batchUpdateAudiobooks.bind(this)) | ||||
| 
 | ||||
|     this.router.get('/audiobook/:id', this.getAudiobook.bind(this)) | ||||
|     this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this)) | ||||
| @ -88,10 +90,7 @@ class ApiController { | ||||
|     res.json(audiobook.toJSONExpanded()) | ||||
|   } | ||||
| 
 | ||||
|   async deleteAudiobook(req, res) { | ||||
|     var audiobook = this.db.audiobooks.find(a => a.id === req.params.id) | ||||
|     if (!audiobook) return res.sendStatus(404) | ||||
| 
 | ||||
|   async handleDeleteAudiobook(audiobook) { | ||||
|     // Remove audiobook from users
 | ||||
|     for (let i = 0; i < this.db.users.length; i++) { | ||||
|       var user = this.db.users[i] | ||||
| @ -114,12 +113,66 @@ class ApiController { | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     var audiobookJSON = audiobook.toJSONMinified() | ||||
|     await this.db.removeEntity('audiobook', audiobook.id) | ||||
|     this.emitter('audiobook_removed', audiobookJSON) | ||||
|   } | ||||
| 
 | ||||
|     this.emitter('audiobook_removed', audiobook.toJSONMinified()) | ||||
|   async deleteAudiobook(req, res) { | ||||
|     var audiobook = this.db.audiobooks.find(a => a.id === req.params.id) | ||||
|     if (!audiobook) return res.sendStatus(404) | ||||
| 
 | ||||
|     await this.handleDeleteAudiobook(audiobook) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   async batchDeleteAudiobooks(req, res) { | ||||
|     var { audiobookIds } = req.body | ||||
|     if (!audiobookIds || !audiobookIds.length) { | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
| 
 | ||||
|     var audiobooksToDelete = this.db.audiobooks.filter(ab => audiobookIds.includes(ab.id)) | ||||
|     if (!audiobooksToDelete.length) { | ||||
|       return res.sendStatus(404) | ||||
|     } | ||||
|     for (let i = 0; i < audiobooksToDelete.length; i++) { | ||||
|       Logger.info(`[ApiController] Deleting Audiobook "${audiobooksToDelete[i].title}"`) | ||||
|       await this.handleDeleteAudiobook(audiobooksToDelete[i]) | ||||
|     } | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|   async batchUpdateAudiobooks(req, res) { | ||||
|     var audiobooks = req.body | ||||
|     if (!audiobooks || !audiobooks.length) { | ||||
|       return res.sendStatus(500) | ||||
|     } | ||||
| 
 | ||||
|     var audiobooksUpdated = 0 | ||||
|     audiobooks = audiobooks.map((ab) => { | ||||
|       var _ab = this.db.audiobooks.find(__ab => __ab.id === ab.id) | ||||
|       if (!_ab) return null | ||||
|       var hasUpdated = _ab.update(ab) | ||||
|       if (!hasUpdated) return null | ||||
|       audiobooksUpdated++ | ||||
|       return _ab | ||||
|     }).filter(ab => ab) | ||||
| 
 | ||||
|     if (audiobooksUpdated) { | ||||
|       Logger.info(`[ApiController] ${audiobooksUpdated} Audiobooks have updates`) | ||||
|       for (let i = 0; i < audiobooks.length; i++) { | ||||
|         await this.db.updateAudiobook(audiobooks[i]) | ||||
|         this.emitter('audiobook_updated', audiobooks[i].toJSONMinified()) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     res.json({ | ||||
|       success: true, | ||||
|       updates: audiobooksUpdated | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async updateAudiobookTracks(req, res) { | ||||
|     var audiobook = this.db.audiobooks.find(a => a.id === req.params.id) | ||||
|     if (!audiobook) return res.sendStatus(404) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user