mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	This commit is contained in:
		
							parent
							
								
									7c1789a7c2
								
							
						
					
					
						commit
						ad4dad1c29
					
				| @ -6,11 +6,11 @@ | |||||||
|         <div class="flex items-center"> |         <div class="flex items-center"> | ||||||
|           <h1>{{ book.title }}</h1> |           <h1>{{ book.title }}</h1> | ||||||
|           <div class="flex-grow" /> |           <div class="flex-grow" /> | ||||||
|           <p>{{ book.year || book.first_publish_date }}</p> |           <p>{{ book.publishYear }}</p> | ||||||
|         </div> |         </div> | ||||||
|         <p class="text-gray-400">{{ book.author }}</p> |         <p class="text-gray-400">{{ book.author }}</p> | ||||||
|         <div class="w-full max-h-12 overflow-hidden"> |         <div class="w-full max-h-12 overflow-hidden"> | ||||||
|           <p class="text-gray-500 text-xs" v-html="book.description"></p> |           <p class="text-gray-500 text-xs">{{ book.description }}</p> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| @ -53,7 +53,7 @@ export default { | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     this.selectedCover = this.bookCovers.length ? this.bookCovers[0] : null |     this.selectedCover = this.bookCovers.length ? this.bookCovers[0] : this.book.cover || null | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </script> | </script> | ||||||
| @ -20,7 +20,7 @@ | |||||||
| 
 | 
 | ||||||
|     <div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300"> |     <div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300"> | ||||||
|       <keep-alive> |       <keep-alive> | ||||||
|         <component v-if="audiobook" :is="tabName" :audiobook="audiobook" :processing.sync="processing" @close="show = false" /> |         <component v-if="audiobook" :is="tabName" :audiobook="audiobook" :processing.sync="processing" @close="show = false" @selectTab="selectTab" /> | ||||||
|       </keep-alive> |       </keep-alive> | ||||||
|     </div> |     </div> | ||||||
|   </modals-modal> |   </modals-modal> | ||||||
| @ -44,11 +44,6 @@ export default { | |||||||
|           title: 'Cover', |           title: 'Cover', | ||||||
|           component: 'modals-edit-tabs-cover' |           component: 'modals-edit-tabs-cover' | ||||||
|         }, |         }, | ||||||
|         // { |  | ||||||
|         //   id: 'match', |  | ||||||
|         //   title: 'Match', |  | ||||||
|         //   component: 'modals-edit-tabs-match' |  | ||||||
|         // }, |  | ||||||
|         { |         { | ||||||
|           id: 'tracks', |           id: 'tracks', | ||||||
|           title: 'Tracks', |           title: 'Tracks', | ||||||
| @ -68,6 +63,11 @@ export default { | |||||||
|           id: 'download', |           id: 'download', | ||||||
|           title: 'Download', |           title: 'Download', | ||||||
|           component: 'modals-edit-tabs-download' |           component: 'modals-edit-tabs-download' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           id: 'match', | ||||||
|  |           title: 'Match', | ||||||
|  |           component: 'modals-edit-tabs-match' | ||||||
|         } |         } | ||||||
|       ] |       ] | ||||||
|     } |     } | ||||||
| @ -123,12 +123,16 @@ export default { | |||||||
|     userCanDownload() { |     userCanDownload() { | ||||||
|       return this.$store.getters['user/getUserCanDownload'] |       return this.$store.getters['user/getUserCanDownload'] | ||||||
|     }, |     }, | ||||||
|  |     showExperimentalFeatures() { | ||||||
|  |       return this.$store.state.showExperimentalFeatures | ||||||
|  |     }, | ||||||
|     availableTabs() { |     availableTabs() { | ||||||
|       if (!this.userCanUpdate && !this.userCanDownload) return [] |       if (!this.userCanUpdate && !this.userCanDownload) return [] | ||||||
|       return this.tabs.filter((tab) => { |       return this.tabs.filter((tab) => { | ||||||
|         if (tab.id === 'download' && this.isMissing) return false |         if (tab.id === 'download' && this.isMissing) return false | ||||||
|         if ((tab.id === 'download' || tab.id === 'tracks') && this.userCanDownload) return true |         if ((tab.id === 'download' || tab.id === 'tracks') && this.userCanDownload) return true | ||||||
|         if (tab.id !== 'download' && tab.id !== 'tracks' && this.userCanUpdate) return true |         if (tab.id !== 'download' && tab.id !== 'tracks' && this.userCanUpdate) return true | ||||||
|  |         if (tab.id === 'match' && this.showExperimentalFeatures) return true | ||||||
|         return false |         return false | ||||||
|       }) |       }) | ||||||
|     }, |     }, | ||||||
| @ -194,7 +198,9 @@ export default { | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     selectTab(tab) { |     selectTab(tab) { | ||||||
|       this.selectedTab = tab |       if (this.availableTabs.find((t) => t.id === tab)) { | ||||||
|  |         this.selectedTab = tab | ||||||
|  |       } | ||||||
|     }, |     }, | ||||||
|     audiobookUpdated() { |     audiobookUpdated() { | ||||||
|       if (!this.show) this.fetchOnShow = true |       if (!this.show) this.fetchOnShow = true | ||||||
|  | |||||||
| @ -1,15 +1,17 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="w-full h-full overflow-hidden px-4 py-6"> |   <div class="w-full h-full overflow-hidden px-4 py-6 relative"> | ||||||
|     <form @submit.prevent="submitSearch"> |     <form @submit.prevent="submitSearch"> | ||||||
|       <div class="flex items-center justify-start -mx-1 h-20"> |       <div class="flex items-center justify-start -mx-1 h-20"> | ||||||
|         <div class="w-72 px-1"> |         <div class="w-40 px-1"> | ||||||
|           <ui-text-input-with-label v-model="searchTitle" label="Search Title" placeholder="Search" :disabled="processing" /> |           <ui-dropdown v-model="provider" :items="providers" label="Provider" small /> | ||||||
|         </div> |         </div> | ||||||
|         <div class="w-72 px-1"> |         <div class="w-72 px-1"> | ||||||
|           <ui-text-input-with-label v-model="searchAuthor" label="Author" :disabled="processing" /> |           <ui-text-input-with-label v-model="searchTitle" label="Search Title" placeholder="Search" /> | ||||||
|  |         </div> | ||||||
|  |         <div class="w-72 px-1"> | ||||||
|  |           <ui-text-input-with-label v-model="searchAuthor" label="Author" /> | ||||||
|         </div> |         </div> | ||||||
|         <ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn> |         <ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn> | ||||||
|         <div class="flex-grow" /> |  | ||||||
|       </div> |       </div> | ||||||
|     </form> |     </form> | ||||||
|     <div v-show="processing" class="flex h-full items-center justify-center"> |     <div v-show="processing" class="flex h-full items-center justify-center"> | ||||||
| @ -23,6 +25,51 @@ | |||||||
|         <cards-book-match-card :key="index" :book="res" @select="selectMatch" /> |         <cards-book-match-card :key="index" :book="res" @select="selectMatch" /> | ||||||
|       </template> |       </template> | ||||||
|     </div> |     </div> | ||||||
|  |     <div v-if="selectedMatch" class="absolute top-0 left-0 w-full bg-bg h-full p-8 max-h-full overflow-y-auto overflow-x-hidden"> | ||||||
|  |       <div class="flex mb-2"> | ||||||
|  |         <div class="w-8 h-8 rounded-full hover:bg-white hover:bg-opacity-10 flex items-center justify-center cursor-pointer" @click="selectedMatch = null"> | ||||||
|  |           <span class="material-icons text-3xl">arrow_back</span> | ||||||
|  |         </div> | ||||||
|  |         <p class="text-xl pl-3">Update Book Details</p> | ||||||
|  |       </div> | ||||||
|  |       <form @submit.prevent="submitMatchUpdate"> | ||||||
|  |         <div v-if="selectedMatch.cover" class="flex items-center py-2"> | ||||||
|  |           <ui-checkbox v-model="selectedMatchUsage.cover" /> | ||||||
|  |           <ui-text-input-with-label v-model="selectedMatch.cover" :disabled="!selectedMatchUsage.cover" label="Cover" class="flex-grow ml-4" /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="selectedMatch.title" class="flex items-center py-2"> | ||||||
|  |           <ui-checkbox v-model="selectedMatchUsage.title" /> | ||||||
|  |           <ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" label="Title" class="flex-grow ml-4" /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="selectedMatch.subtitle" class="flex items-center py-2"> | ||||||
|  |           <ui-checkbox v-model="selectedMatchUsage.subtitle" /> | ||||||
|  |           <ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" label="Subtitle" class="flex-grow ml-4" /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="selectedMatch.author" class="flex items-center py-2"> | ||||||
|  |           <ui-checkbox v-model="selectedMatchUsage.author" /> | ||||||
|  |           <ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" label="Author" class="flex-grow ml-4" /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="selectedMatch.description" class="flex items-center py-2"> | ||||||
|  |           <ui-checkbox v-model="selectedMatchUsage.description" /> | ||||||
|  |           <ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" label="Description" class="flex-grow ml-4" /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="selectedMatch.publisher" class="flex items-center py-2"> | ||||||
|  |           <ui-checkbox v-model="selectedMatchUsage.publisher" /> | ||||||
|  |           <ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" label="Publisher" class="flex-grow ml-4" /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="selectedMatch.publishYear" class="flex items-center py-2"> | ||||||
|  |           <ui-checkbox v-model="selectedMatchUsage.publishYear" /> | ||||||
|  |           <ui-text-input-with-label v-model="selectedMatch.publishYear" :disabled="!selectedMatchUsage.publishYear" label="Publish Year" class="flex-grow ml-4" /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="selectedMatch.isbn" class="flex items-center py-2"> | ||||||
|  |           <ui-checkbox v-model="selectedMatchUsage.isbn" /> | ||||||
|  |           <ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" class="flex-grow ml-4" /> | ||||||
|  |         </div> | ||||||
|  |         <div class="flex items-center justify-end py-2"> | ||||||
|  |           <ui-btn color="success" type="submit">Update</ui-btn> | ||||||
|  |         </div> | ||||||
|  |       </form> | ||||||
|  |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| @ -41,9 +88,30 @@ export default { | |||||||
|       searchTitle: null, |       searchTitle: null, | ||||||
|       searchAuthor: null, |       searchAuthor: null, | ||||||
|       lastSearch: null, |       lastSearch: null, | ||||||
|       provider: 'best', |       providers: [ | ||||||
|  |         { | ||||||
|  |           text: 'Google Books', | ||||||
|  |           value: 'google' | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |           text: 'Open Library', | ||||||
|  |           value: 'openlibrary' | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       provider: 'google', | ||||||
|       searchResults: [], |       searchResults: [], | ||||||
|       hasSearched: false |       hasSearched: false, | ||||||
|  |       selectedMatch: null, | ||||||
|  |       selectedMatchUsage: { | ||||||
|  |         title: true, | ||||||
|  |         subtitle: true, | ||||||
|  |         cover: true, | ||||||
|  |         author: true, | ||||||
|  |         description: true, | ||||||
|  |         isbn: true, | ||||||
|  |         publisher: true, | ||||||
|  |         publishYear: true | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   watch: { |   watch: { | ||||||
| @ -95,6 +163,18 @@ export default { | |||||||
|       this.hasSearched = true |       this.hasSearched = true | ||||||
|     }, |     }, | ||||||
|     init() { |     init() { | ||||||
|  |       this.selectedMatch = null | ||||||
|  |       this.selectedMatchUsage = { | ||||||
|  |         title: true, | ||||||
|  |         subtitle: true, | ||||||
|  |         cover: true, | ||||||
|  |         author: true, | ||||||
|  |         description: true, | ||||||
|  |         isbn: true, | ||||||
|  |         publisher: true, | ||||||
|  |         publishYear: true | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       if (this.audiobook.id !== this.audiobookId) { |       if (this.audiobook.id !== this.audiobookId) { | ||||||
|         this.searchResults = [] |         this.searchResults = [] | ||||||
|         this.hasSearched = false |         this.hasSearched = false | ||||||
| @ -107,31 +187,63 @@ export default { | |||||||
|         return |         return | ||||||
|       } |       } | ||||||
|       this.searchTitle = this.audiobook.book.title |       this.searchTitle = this.audiobook.book.title | ||||||
|       this.searchAuthor = this.audiobook.book.author || '' |       this.searchAuthor = this.audiobook.book.authorFL || '' | ||||||
|     }, |     }, | ||||||
|     async selectMatch(match) { |     selectMatch(match) { | ||||||
|  |       this.selectedMatch = match | ||||||
|  |     }, | ||||||
|  |     buildMatchUpdatePayload() { | ||||||
|  |       var updatePayload = {} | ||||||
|  |       for (const key in this.selectedMatchUsage) { | ||||||
|  |         if (this.selectedMatchUsage[key] && this.selectedMatch[key]) { | ||||||
|  |           updatePayload[key] = this.selectedMatch[key] | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       return updatePayload | ||||||
|  |     }, | ||||||
|  |     async submitMatchUpdate() { | ||||||
|  |       var updatePayload = this.buildMatchUpdatePayload() | ||||||
|  |       if (!Object.keys(updatePayload).length) { | ||||||
|  |         return | ||||||
|  |       } | ||||||
|       this.isProcessing = true |       this.isProcessing = true | ||||||
|       const updatePayload = { | 
 | ||||||
|         book: {} |       if (updatePayload.cover) { | ||||||
|  |         var coverPayload = { | ||||||
|  |           url: updatePayload.cover | ||||||
|  |         } | ||||||
|  |         var success = await this.$axios.$post(`/api/audiobook/${this.audiobook.id}/cover`, coverPayload).catch((error) => { | ||||||
|  |           console.error('Failed to update', error) | ||||||
|  |           return false | ||||||
|  |         }) | ||||||
|  |         if (success) { | ||||||
|  |           this.$toast.success('Book Cover Updated') | ||||||
|  |         } else { | ||||||
|  |           this.$toast.error('Book Cover Failed to Update') | ||||||
|  |         } | ||||||
|  |         console.log('Updated cover') | ||||||
|  |         delete updatePayload.cover | ||||||
|       } |       } | ||||||
|       if (match.cover) { | 
 | ||||||
|         updatePayload.book.cover = match.cover |       if (Object.keys(updatePayload).length) { | ||||||
|  |         var bookUpdatePayload = { | ||||||
|  |           book: updatePayload | ||||||
|  |         } | ||||||
|  |         var success = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, bookUpdatePayload).catch((error) => { | ||||||
|  |           console.error('Failed to update', error) | ||||||
|  |           return false | ||||||
|  |         }) | ||||||
|  |         if (success) { | ||||||
|  |           this.$toast.success('Book Details Updated') | ||||||
|  |           this.selectedMatch = null | ||||||
|  |           this.$emit('selectTab', 'details') | ||||||
|  |         } else { | ||||||
|  |           this.$toast.error('Book Details Failed to Update') | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         this.selectedMatch = null | ||||||
|       } |       } | ||||||
|       if (match.title) { |  | ||||||
|         updatePayload.book.title = match.title |  | ||||||
|       } |  | ||||||
|       if (match.description) { |  | ||||||
|         updatePayload.book.description = match.description |  | ||||||
|       } |  | ||||||
|       var updatedAudiobook = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, updatePayload).catch((error) => { |  | ||||||
|         console.error('Failed to update', error) |  | ||||||
|         return false |  | ||||||
|       }) |  | ||||||
|       this.isProcessing = false |       this.isProcessing = false | ||||||
|       if (updatedAudiobook) { |  | ||||||
|         this.$toast.success('Update Successful') |  | ||||||
|         this.$emit('close') |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										33
									
								
								client/components/ui/Checkbox.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								client/components/ui/Checkbox.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | |||||||
|  | <template> | ||||||
|  |   <label class="flex justify-start items-start"> | ||||||
|  |     <div class="bg-white border-2 rounded border-gray-400 w-6 h-6 flex flex-shrink-0 justify-center items-center focus-within:border-blue-500"> | ||||||
|  |       <input v-model="selected" type="checkbox" class="opacity-0 absolute" /> | ||||||
|  |       <svg v-if="selected" class="fill-current w-4 h-4 text-green-500 pointer-events-none" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg> | ||||||
|  |     </div> | ||||||
|  |     <div v-if="label" class="select-none">{{ label }}</div> | ||||||
|  |   </label> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     value: Boolean, | ||||||
|  |     label: Boolean | ||||||
|  |   }, | ||||||
|  |   data() { | ||||||
|  |     return {} | ||||||
|  |   }, | ||||||
|  |   computed: { | ||||||
|  |     selected: { | ||||||
|  |       get() { | ||||||
|  |         return this.value | ||||||
|  |       }, | ||||||
|  |       set(val) { | ||||||
|  |         this.$emit('input', !!val) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   methods: {}, | ||||||
|  |   mounted() {} | ||||||
|  | } | ||||||
|  | </script> | ||||||
| @ -1,9 +1,9 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="relative w-full" v-click-outside="clickOutside"> |   <div class="relative w-full" v-click-outside="clickOutside"> | ||||||
|     <p class="text-sm text-opacity-75 mb-1">{{ label }}</p> |     <p class="text-sm font-semibold">{{ label }}</p> | ||||||
|     <button type="button" :disabled="disabled" class="relative h-10 w-full border border-gray-500 rounded shadow-sm pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer bg-primary" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu"> |     <button type="button" :disabled="disabled" class="relative w-full border border-gray-500 rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none sm:text-sm cursor-pointer bg-primary" :class="small ? 'h-9' : 'h-10'" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu"> | ||||||
|       <span class="flex items-center"> |       <span class="flex items-center"> | ||||||
|         <span class="block truncate">{{ selectedText }}</span> |         <span class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText }}</span> | ||||||
|       </span> |       </span> | ||||||
|       <span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> |       <span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> | ||||||
|         <span class="material-icons text-gray-100">expand_more</span> |         <span class="material-icons text-gray-100">expand_more</span> | ||||||
| @ -36,7 +36,8 @@ export default { | |||||||
|       type: Array, |       type: Array, | ||||||
|       default: () => [] |       default: () => [] | ||||||
|     }, |     }, | ||||||
|     disabled: Boolean |     disabled: Boolean, | ||||||
|  |     small: Boolean | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|  | |||||||
| @ -80,6 +80,7 @@ input { | |||||||
|   border-style: inherit !important; |   border-style: inherit !important; | ||||||
| } | } | ||||||
| input:read-only { | input:read-only { | ||||||
|  |   color: #aaa; | ||||||
|   background-color: #444; |   background-color: #444; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
| @ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="w-full"> |   <div class="w-full"> | ||||||
|     <p class="px-1 text-sm font-semibold"> |     <p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''"> | ||||||
|       {{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em> |       {{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em> | ||||||
|     </p> |     </p> | ||||||
|     <ui-text-input v-model="inputValue" :disabled="disabled" :type="type" class="w-full" /> |     <ui-text-input v-model="inputValue" :disabled="disabled" :type="type" class="w-full" /> | ||||||
|  | |||||||
| @ -38,10 +38,11 @@ export default { | |||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style scoped> | <style scoped> | ||||||
| input { | textarea { | ||||||
|   border-style: inherit !important; |   border-style: inherit !important; | ||||||
| } | } | ||||||
| input:read-only { | textarea:read-only { | ||||||
|   background-color: #eee; |   color: #aaa; | ||||||
|  |   background-color: #444; | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
| @ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="w-full"> |   <div class="w-full"> | ||||||
|     <p class="px-1 text-sm font-semibold">{{ label }}</p> |     <p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p> | ||||||
|     <ui-textarea-input v-model="inputValue" :rows="rows" class="w-full" /> |     <ui-textarea-input v-model="inputValue" :disabled="disabled" :rows="rows" class="w-full" /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| @ -10,6 +10,7 @@ export default { | |||||||
|   props: { |   props: { | ||||||
|     value: [String, Number], |     value: [String, Number], | ||||||
|     label: String, |     label: String, | ||||||
|  |     disabled: Boolean, | ||||||
|     rows: { |     rows: { | ||||||
|       type: Number, |       type: Number, | ||||||
|       default: 2 |       default: 2 | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "audiobookshelf-client", |   "name": "audiobookshelf-client", | ||||||
|   "version": "1.5.5", |   "version": "1.5.6", | ||||||
|   "description": "Audiobook manager and player", |   "description": "Audiobook manager and player", | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "audiobookshelf", |   "name": "audiobookshelf", | ||||||
|   "version": "1.5.5", |   "version": "1.5.6", | ||||||
|   "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": { | ||||||
|  | |||||||
| @ -6,6 +6,8 @@ const Logger = require('./Logger') | |||||||
| const { isObject } = require('./utils/index') | const { isObject } = require('./utils/index') | ||||||
| const audioFileScanner = require('./utils/audioFileScanner') | const audioFileScanner = require('./utils/audioFileScanner') | ||||||
| 
 | 
 | ||||||
|  | const BookFinder = require('./BookFinder') | ||||||
|  | 
 | ||||||
| const Library = require('./objects/Library') | const Library = require('./objects/Library') | ||||||
| const User = require('./objects/User') | const User = require('./objects/User') | ||||||
| 
 | 
 | ||||||
| @ -24,6 +26,8 @@ class ApiController { | |||||||
|     this.clientEmitter = clientEmitter |     this.clientEmitter = clientEmitter | ||||||
|     this.MetadataPath = MetadataPath |     this.MetadataPath = MetadataPath | ||||||
| 
 | 
 | ||||||
|  |     this.bookFinder = new BookFinder() | ||||||
|  | 
 | ||||||
|     this.router = express() |     this.router = express() | ||||||
|     this.init() |     this.init() | ||||||
|   } |   } | ||||||
| @ -51,6 +55,7 @@ class ApiController { | |||||||
|     this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this)) |     this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this)) | ||||||
|     this.router.post('/audiobook/:id/cover', this.uploadAudiobookCover.bind(this)) |     this.router.post('/audiobook/:id/cover', this.uploadAudiobookCover.bind(this)) | ||||||
|     this.router.patch('/audiobook/:id/coverfile', this.updateAudiobookCoverFromFile.bind(this)) |     this.router.patch('/audiobook/:id/coverfile', this.updateAudiobookCoverFromFile.bind(this)) | ||||||
|  |     this.router.get('/audiobook/:id/match', this.matchAudiobookBook.bind(this)) | ||||||
|     this.router.patch('/audiobook/:id', this.updateAudiobook.bind(this)) |     this.router.patch('/audiobook/:id', this.updateAudiobook.bind(this)) | ||||||
| 
 | 
 | ||||||
|     this.router.patch('/match/:id', this.match.bind(this)) |     this.router.patch('/match/:id', this.match.bind(this)) | ||||||
| @ -85,8 +90,12 @@ class ApiController { | |||||||
|     this.router.get('/scantracks/:id', this.scanAudioTrackNums.bind(this)) |     this.router.get('/scantracks/:id', this.scanAudioTrackNums.bind(this)) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   find(req, res) { |   async find(req, res) { | ||||||
|     this.scanner.find(req, res) |     var provider = req.query.provider || 'google' | ||||||
|  |     var title = req.query.title || '' | ||||||
|  |     var author = req.query.author || '' | ||||||
|  |     var results = await this.bookFinder.search(provider, title, author) | ||||||
|  |     res.json(results) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   findCovers(req, res) { |   findCovers(req, res) { | ||||||
| @ -497,6 +506,18 @@ class ApiController { | |||||||
|     else res.status(200).send('No update was made to cover') |     else res.status(200).send('No update was made to cover') | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async matchAudiobookBook(req, res) { | ||||||
|  |     var audiobook = this.db.audiobooks.find(a => a.id === req.params.id) | ||||||
|  |     if (!audiobook) return res.sendStatus(404) | ||||||
|  | 
 | ||||||
|  |     var provider = req.query.provider || 'google' | ||||||
|  |     var excludeAuthor = req.query.excludeAuthor === '1' | ||||||
|  |     var authorSearch = excludeAuthor ? null : audiobook.authorFL | ||||||
|  | 
 | ||||||
|  |     var results = await this.bookFinder.search(provider, audiobook.title, authorSearch) | ||||||
|  |     res.json(results) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async updateAudiobook(req, res) { |   async updateAudiobook(req, res) { | ||||||
|     if (!req.user.canUpdate) { |     if (!req.user.canUpdate) { | ||||||
|       Logger.warn('User attempted to update without permission', req.user) |       Logger.warn('User attempted to update without permission', req.user) | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| const OpenLibrary = require('./providers/OpenLibrary') | const OpenLibrary = require('./providers/OpenLibrary') | ||||||
| const LibGen = require('./providers/LibGen') | const LibGen = require('./providers/LibGen') | ||||||
|  | const GoogleBooks = require('./providers/GoogleBooks') | ||||||
| const Logger = require('./Logger') | const Logger = require('./Logger') | ||||||
| const { levenshteinDistance } = require('./utils/index') | const { levenshteinDistance } = require('./utils/index') | ||||||
| 
 | 
 | ||||||
| @ -7,6 +8,7 @@ class BookFinder { | |||||||
|   constructor() { |   constructor() { | ||||||
|     this.openLibrary = new OpenLibrary() |     this.openLibrary = new OpenLibrary() | ||||||
|     this.libGen = new LibGen() |     this.libGen = new LibGen() | ||||||
|  |     this.googleBooks = new GoogleBooks() | ||||||
| 
 | 
 | ||||||
|     this.verbose = false |     this.verbose = false | ||||||
|   } |   } | ||||||
| @ -143,13 +145,26 @@ class BookFinder { | |||||||
|     return booksFiltered |     return booksFiltered | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async getGoogleBooksResults(title, author, maxTitleDistance, maxAuthorDistance) { | ||||||
|  |     var books = await this.googleBooks.search(title, author) | ||||||
|  |     if (this.verbose) Logger.debug(`GoogleBooks Book Search Results: ${books.length || 0}`) | ||||||
|  |     if (books.errorCode) { | ||||||
|  |       Logger.error(`GoogleBooks Search Error ${books.errorCode}`) | ||||||
|  |       return [] | ||||||
|  |     } | ||||||
|  |     // Google has good sort
 | ||||||
|  |     return books | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async search(provider, title, author, options = {}) { |   async search(provider, title, author, options = {}) { | ||||||
|     var books = [] |     var books = [] | ||||||
|     var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4 |     var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4 | ||||||
|     var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4 |     var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4 | ||||||
|     Logger.debug(`Cover Search: title: "${title}", author: "${author}", provider: ${provider}`) |     Logger.debug(`Cover Search: title: "${title}", author: "${author}", provider: ${provider}`) | ||||||
| 
 | 
 | ||||||
|     if (provider === 'libgen') { |     if (provider === 'google') { | ||||||
|  |       return this.getGoogleBooksResults(title, author, maxTitleDistance, maxAuthorDistance) | ||||||
|  |     } else if (provider === 'libgen') { | ||||||
|       books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) |       books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) | ||||||
|     } else if (provider === 'openlibrary') { |     } else if (provider === 'openlibrary') { | ||||||
|       books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) |       books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) | ||||||
|  | |||||||
| @ -10,7 +10,6 @@ const { comparePaths, getIno } = require('./utils/index') | |||||||
| const { secondsToTimestamp } = require('./utils/fileUtils') | const { secondsToTimestamp } = require('./utils/fileUtils') | ||||||
| const { ScanResult, CoverDestination } = require('./utils/constants') | const { ScanResult, CoverDestination } = require('./utils/constants') | ||||||
| 
 | 
 | ||||||
| // Classes
 |  | ||||||
| const BookFinder = require('./BookFinder') | const BookFinder = require('./BookFinder') | ||||||
| const Audiobook = require('./objects/Audiobook') | const Audiobook = require('./objects/Audiobook') | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -91,6 +91,10 @@ class Audiobook { | |||||||
|     return this.book ? this.book.authorLF : null |     return this.book ? this.book.authorLF : null | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   get authorFL() { | ||||||
|  |     return this.book ? this.book.authorFL : null | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   get genres() { |   get genres() { | ||||||
|     return this.book ? this.book.genres || [] : [] |     return this.book ? this.book.genres || [] : [] | ||||||
|   } |   } | ||||||
|  | |||||||
| @ -1,4 +1,3 @@ | |||||||
| const fs = require('fs-extra') |  | ||||||
| const Path = require('path') | const Path = require('path') | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const parseAuthors = require('../utils/parseAuthors') | const parseAuthors = require('../utils/parseAuthors') | ||||||
| @ -16,6 +15,7 @@ class Book { | |||||||
|     this.publishYear = null |     this.publishYear = null | ||||||
|     this.publisher = null |     this.publisher = null | ||||||
|     this.description = null |     this.description = null | ||||||
|  |     this.isbn = null | ||||||
|     this.cover = null |     this.cover = null | ||||||
|     this.coverFullPath = null |     this.coverFullPath = null | ||||||
|     this.genres = [] |     this.genres = [] | ||||||
| @ -56,6 +56,7 @@ class Book { | |||||||
|     this.publishYear = book.publishYear |     this.publishYear = book.publishYear | ||||||
|     this.publisher = book.publisher |     this.publisher = book.publisher | ||||||
|     this.description = book.description |     this.description = book.description | ||||||
|  |     this.isbn = book.isbn || null | ||||||
|     this.cover = book.cover |     this.cover = book.cover | ||||||
|     this.coverFullPath = book.coverFullPath || null |     this.coverFullPath = book.coverFullPath || null | ||||||
|     this.genres = book.genres |     this.genres = book.genres | ||||||
| @ -78,6 +79,7 @@ class Book { | |||||||
|       publishYear: this.publishYear, |       publishYear: this.publishYear, | ||||||
|       publisher: this.publisher, |       publisher: this.publisher, | ||||||
|       description: this.description, |       description: this.description, | ||||||
|  |       isbn: this.isbn, | ||||||
|       cover: this.cover, |       cover: this.cover, | ||||||
|       coverFullPath: this.coverFullPath, |       coverFullPath: this.coverFullPath, | ||||||
|       genres: this.genres, |       genres: this.genres, | ||||||
| @ -116,6 +118,7 @@ class Book { | |||||||
|     this.volumeNumber = data.volumeNumber || null |     this.volumeNumber = data.volumeNumber || null | ||||||
|     this.publishYear = data.publishYear || null |     this.publishYear = data.publishYear || null | ||||||
|     this.description = data.description || null |     this.description = data.description || null | ||||||
|  |     this.isbn = data.isbn || null | ||||||
|     this.cover = data.cover || null |     this.cover = data.cover || null | ||||||
|     this.coverFullPath = data.coverFullPath || null |     this.coverFullPath = data.coverFullPath || null | ||||||
|     this.genres = data.genres || [] |     this.genres = data.genres || [] | ||||||
|  | |||||||
							
								
								
									
										50
									
								
								server/providers/GoogleBooks.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								server/providers/GoogleBooks.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,50 @@ | |||||||
|  | const axios = require('axios') | ||||||
|  | const Logger = require('../Logger') | ||||||
|  | 
 | ||||||
|  | class GoogleBooks { | ||||||
|  |   constructor() { } | ||||||
|  | 
 | ||||||
|  |   extractIsbn(industryIdentifiers) { | ||||||
|  |     if (!industryIdentifiers || !industryIdentifiers.length) return null | ||||||
|  | 
 | ||||||
|  |     var isbnObj = industryIdentifiers.find(i => i.type === 'ISBN_13') || industryIdentifiers.find(i => i.type === 'ISBN_10') | ||||||
|  |     if (isbnObj && isbnObj.identifier) return isbnObj.identifier | ||||||
|  |     return null | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   cleanResult(item) { | ||||||
|  |     var { id, volumeInfo } = item | ||||||
|  |     if (!volumeInfo) return null | ||||||
|  |     var { title, subtitle, authors, publisher, publisherDate, description, industryIdentifiers, categories, imageLinks } = volumeInfo | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       id, | ||||||
|  |       title, | ||||||
|  |       subtitle: subtitle || null, | ||||||
|  |       author: authors ? authors.join(', ') : null, | ||||||
|  |       publisher, | ||||||
|  |       publishYear: publisherDate ? publisherDate.split('-')[0] : null, | ||||||
|  |       description, | ||||||
|  |       cover: imageLinks && imageLinks.thumbnail ? imageLinks.thumbnail : null, | ||||||
|  |       genres: categories ? categories.join(', ') : null, | ||||||
|  |       isbn: this.extractIsbn(industryIdentifiers) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async search(title, author) { | ||||||
|  |     var queryString = `q=intitle:${title}` | ||||||
|  |     if (author) queryString += `+inauthor:${author}` | ||||||
|  |     var url = `https://www.googleapis.com/books/v1/volumes?${queryString}` | ||||||
|  |     Logger.debug(`[GoogleBooks] Search url: ${url}`) | ||||||
|  |     var items = await axios.get(url).then((res) => { | ||||||
|  |       if (!res || !res.data || !res.data.items) return [] | ||||||
|  |       return res.data.items | ||||||
|  |     }).catch(error => { | ||||||
|  |       Logger.error('[GoogleBooks] Volume search error', error) | ||||||
|  |       return [] | ||||||
|  |     }) | ||||||
|  |     return items.map(item => this.cleanResult(item)) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports = GoogleBooks | ||||||
| @ -51,12 +51,21 @@ class OpenLibrary { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   parsePublishYear(doc, worksData) { | ||||||
|  |     if (doc.first_publish_year && !isNaN(doc.first_publish_year)) return doc.first_publish_year | ||||||
|  |     if (worksData.first_publish_date) { | ||||||
|  |       var year = worksData.first_publish_date.split('-')[0] | ||||||
|  |       if (!isNaN(year)) return year | ||||||
|  |     } | ||||||
|  |     return null | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async cleanSearchDoc(doc) { |   async cleanSearchDoc(doc) { | ||||||
|     var worksData = await this.getWorksData(doc.key) |     var worksData = await this.getWorksData(doc.key) | ||||||
|     return { |     return { | ||||||
|       title: doc.title, |       title: doc.title, | ||||||
|       author: doc.author_name ? doc.author_name.join(', ') : null, |       author: doc.author_name ? doc.author_name.join(', ') : null, | ||||||
|       year: doc.first_publish_year, |       publishYear: this.parsePublishYear(doc, worksData), | ||||||
|       edition: doc.cover_edition_key, |       edition: doc.cover_edition_key, | ||||||
|       cover: doc.cover_edition_key ? `https://covers.openlibrary.org/b/OLID/${doc.cover_edition_key}-L.jpg` : null, |       cover: doc.cover_edition_key ? `https://covers.openlibrary.org/b/OLID/${doc.cover_edition_key}-L.jpg` : null, | ||||||
|       ...worksData |       ...worksData | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user