mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update book finder and cover matching - includes LibGen provider
This commit is contained in:
		
							parent
							
								
									744aacbb4b
								
							
						
					
					
						commit
						7d4e2e3d97
					
				| @ -1,5 +1,5 @@ | ||||
| <template> | ||||
|   <div class="relative rounded-sm overflow-hidden" :style="{ height: width * 1.6 + 'px', width: width + 'px' }"> | ||||
|   <div class="relative rounded-sm overflow-hidden" :style="{ height: width * 1.6 + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }"> | ||||
|     <img ref="cover" :src="cover" class="w-full h-full object-cover" /> | ||||
| 
 | ||||
|     <div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }"> | ||||
|  | ||||
							
								
								
									
										59
									
								
								client/components/cards/BookMatchCard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								client/components/cards/BookMatchCard.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| <template> | ||||
|   <div class="w-full border-b border-gray-700 pb-2"> | ||||
|     <div class="flex py-1 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer" @click="selectMatch"> | ||||
|       <img :src="selectedCover || '/book_placeholder.jpg'" class="h-24 object-cover" style="width: 60px" /> | ||||
|       <div class="px-4 flex-grow"> | ||||
|         <div class="flex items-center"> | ||||
|           <h1>{{ book.title }}</h1> | ||||
|           <div class="flex-grow" /> | ||||
|           <p>{{ book.year || book.first_publish_date }}</p> | ||||
|         </div> | ||||
|         <p class="text-gray-400">{{ book.author }}</p> | ||||
|         <div class="w-full max-h-12 overflow-hidden"> | ||||
|           <p class="text-gray-500 text-xs" v-html="book.description"></p> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div v-if="bookCovers.length > 1" class="flex"> | ||||
|       <template v-for="cover in bookCovers"> | ||||
|         <div :key="cover" class="border-2 hover:border-yellow-300 border-transparent" :class="cover === selectedCover ? 'border-yellow-200' : ''" @mousedown.stop @mouseup.stop @click.stop="clickCover(cover)"> | ||||
|           <img :src="cover" class="h-20 w-12 object-cover mr-1" /> | ||||
|         </div> | ||||
|       </template> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <script> | ||||
| export default { | ||||
|   props: { | ||||
|     book: { | ||||
|       type: Object, | ||||
|       default: () => {} | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       selectedCover: null | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     bookCovers() { | ||||
|       return this.book.covers ? this.book.covers || [] : [] | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     selectMatch() { | ||||
|       var book = { ...this.book } | ||||
|       book.cover = this.selectedCover | ||||
|       this.$emit('select', this.book) | ||||
|     }, | ||||
|     clickCover(cover) { | ||||
|       this.selectedCover = cover | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.selectedCover = this.bookCovers.length ? this.bookCovers[0] : null | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -34,7 +34,9 @@ export default { | ||||
|         if (newVal) { | ||||
|           if (this.audiobook && this.audiobook.id === this.selectedAudiobookId) return | ||||
|           this.audiobook = null | ||||
|           this.fetchFull() | ||||
|           this.init() | ||||
|         } else { | ||||
|           this.$store.commit('audiobooks/removeListener', 'edit-modal') | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| @ -72,6 +74,13 @@ export default { | ||||
|     selectTab(tab) { | ||||
|       this.selectedTab = tab | ||||
|     }, | ||||
|     audiobookUpdated() { | ||||
|       this.fetchFull() | ||||
|     }, | ||||
|     init() { | ||||
|       this.$store.commit('audiobooks/addListener', { meth: this.audiobookUpdated, id: 'edit-modal', audiobookId: this.selectedAudiobookId }) | ||||
|       this.fetchFull() | ||||
|     }, | ||||
|     async fetchFull() { | ||||
|       try { | ||||
|         this.audiobook = await this.$axios.$get(`/api/audiobook/${this.selectedAudiobookId}`) | ||||
|  | ||||
| @ -2,13 +2,35 @@ | ||||
|   <div class="w-full h-full"> | ||||
|     <div class="flex"> | ||||
|       <cards-book-cover :audiobook="audiobook" /> | ||||
|       <div class="flex-grow px-8"> | ||||
|       <div class="flex-grow pl-6 pr-2"> | ||||
|         <form @submit.prevent="submitForm"> | ||||
|           <div class="flex items-center"> | ||||
|             <ui-text-input-with-label v-model="imageUrl" label="Cover Image URL" /> | ||||
|             <ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-4">Update</ui-btn> | ||||
|             <ui-btn color="success" type="submit" :padding-x="4" class="mt-5 ml-3 w-24">Update</ui-btn> | ||||
|           </div> | ||||
|         </form> | ||||
| 
 | ||||
|         <form @submit.prevent="submitSearchForm"> | ||||
|           <div class="flex items-center justify-start -mx-1 py-2 mt-2"> | ||||
|             <div class="flex-grow px-1"> | ||||
|               <ui-text-input-with-label v-model="searchTitle" label="Search Title" placeholder="Search" :disabled="processing" /> | ||||
|             </div> | ||||
|             <div class="flex-grow px-1"> | ||||
|               <ui-text-input-with-label v-model="searchAuthor" label="Author" :disabled="processing" /> | ||||
|             </div> | ||||
|             <div class="w-24 px-1"> | ||||
|               <ui-btn type="submit" class="mt-5 w-full" :padding-x="0">Search</ui-btn> | ||||
|             </div> | ||||
|           </div> | ||||
|         </form> | ||||
|         <div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-72 overflow-y-scroll mt-2 max-w-full"> | ||||
|           <p v-if="!coversFound.length">No Covers Found</p> | ||||
|           <template v-for="cover in coversFound"> | ||||
|             <div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="setCover(cover)"> | ||||
|               <img :src="cover" class="h-24 object-cover" style="width: 60px" /> | ||||
|             </div> | ||||
|           </template> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| @ -25,7 +47,11 @@ export default { | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       imageUrl: null | ||||
|       searchTitle: null, | ||||
|       searchAuthor: null, | ||||
|       imageUrl: null, | ||||
|       coversFound: [], | ||||
|       hasSearched: false | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
| @ -51,14 +77,22 @@ export default { | ||||
|   }, | ||||
|   methods: { | ||||
|     init() { | ||||
|       if (this.coversFound.length && (this.searchTitle !== this.book.title || this.searchAuthor !== this.book.author)) { | ||||
|         this.coversFound = [] | ||||
|         this.hasSearched = false | ||||
|       } | ||||
|       this.imageUrl = this.book.cover || '' | ||||
|       this.searchTitle = this.book.title || '' | ||||
|       this.searchAuthor = this.book.author || '' | ||||
|     }, | ||||
|     async submitForm() { | ||||
|       console.log('Submit form', this.details) | ||||
|     submitForm() { | ||||
|       this.updateCover(this.imageUrl) | ||||
|     }, | ||||
|     async updateCover(cover) { | ||||
|       this.isProcessing = true | ||||
|       const updatePayload = { | ||||
|         book: { | ||||
|           cover: this.imageUrl | ||||
|           cover: cover | ||||
|         } | ||||
|       } | ||||
|       var updatedAudiobook = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, updatePayload).catch((error) => { | ||||
| @ -71,6 +105,25 @@ export default { | ||||
|         this.$toast.success('Update Successful') | ||||
|         this.$emit('close') | ||||
|       } | ||||
|     }, | ||||
|     getSearchQuery() { | ||||
|       var searchQuery = `provider=best&title=${this.searchTitle}` | ||||
|       if (this.searchAuthor) searchQuery += `&author=${this.searchAuthor}` | ||||
|       return searchQuery | ||||
|     }, | ||||
|     async submitSearchForm() { | ||||
|       this.isProcessing = true | ||||
|       var searchQuery = this.getSearchQuery() | ||||
|       var results = await this.$axios.$get(`/api/find/covers?${searchQuery}`).catch((error) => { | ||||
|         console.error('Failed', error) | ||||
|         return [] | ||||
|       }) | ||||
|       this.coversFound = results | ||||
|       this.isProcessing = false | ||||
|       this.hasSearched = true | ||||
|     }, | ||||
|     setCover(cover) { | ||||
|       this.updateCover(cover) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,42 +1,26 @@ | ||||
| <template> | ||||
|   <div class="w-full h-full overflow-hidden"> | ||||
|     <div class="flex items-center mb-4"> | ||||
|       <div class="w-72"> | ||||
|         <form @submit.prevent="submitSearch"> | ||||
|           <ui-text-input-with-label v-model="search" label="Search Title" placeholder="Search" :disabled="processing" /> | ||||
|         </form> | ||||
|     <form @submit.prevent="submitSearch"> | ||||
|       <div class="flex items-center justify-start -mx-1 h-20"> | ||||
|         <div class="w-72 px-1"> | ||||
|           <ui-text-input-with-label v-model="searchTitle" label="Search Title" placeholder="Search" :disabled="processing" /> | ||||
|         </div> | ||||
|         <div class="w-72 px-1"> | ||||
|           <ui-text-input-with-label v-model="searchAuthor" label="Author" :disabled="processing" /> | ||||
|         </div> | ||||
|         <ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn> | ||||
|         <div class="flex-grow" /> | ||||
|       </div> | ||||
|       <div class="flex-grow" /> | ||||
|     </div> | ||||
|     </form> | ||||
|     <div v-show="processing" class="flex h-full items-center justify-center"> | ||||
|       <p>Loading...</p> | ||||
|     </div> | ||||
|     <div v-show="!processing && !searchResults.length" class="flex h-full items-center justify-center"> | ||||
|       <p>No Results</p> | ||||
|     </div> | ||||
|     <div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden"> | ||||
|     <div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper"> | ||||
|       <template v-for="(res, index) in searchResults"> | ||||
|         <div :key="index" class="w-full border-b border-gray-700 pb-2 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer" @click="selectMatch(res)"> | ||||
|           <div class="flex py-1"> | ||||
|             <img :src="res.cover || '/book_placeholder.jpg'" class="h-24 object-cover" style="width: 60px" /> | ||||
|             <div class="px-4 flex-grow"> | ||||
|               <div class="flex items-center"> | ||||
|                 <h1>{{ res.title }}</h1> | ||||
|                 <div class="flex-grow" /> | ||||
|                 <p>{{ res.first_publish_year || res.first_publish_date }}</p> | ||||
|               </div> | ||||
|               <p class="text-gray-400">{{ res.author }}</p> | ||||
|               <div class="w-full max-h-12 overflow-hidden"> | ||||
|                 <p class="text-gray-500 text-xs" v-html="res.description"></p> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div v-if="res.covers && res.covers.length > 1" class="flex"> | ||||
|             <template v-for="cover in res.covers.slice(1)"> | ||||
|               <img :key="cover" :src="cover" class="h-20 w-12 object-cover mr-1" /> | ||||
|             </template> | ||||
|           </div> | ||||
|         </div> | ||||
|         <cards-book-match-card :key="index" :book="res" @select="selectMatch" /> | ||||
|       </template> | ||||
|     </div> | ||||
|   </div> | ||||
| @ -53,8 +37,10 @@ export default { | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       search: null, | ||||
|       searchTitle: null, | ||||
|       searchAuthor: null, | ||||
|       lastSearch: null, | ||||
|       provider: 'best', | ||||
|       searchResults: [] | ||||
|     } | ||||
|   }, | ||||
| @ -77,36 +63,41 @@ export default { | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     getSearchQuery() { | ||||
|       var searchQuery = `provider=${this.provider}&title=${this.searchTitle}` | ||||
|       if (this.searchAuthor) searchQuery += `&author=${this.searchAuthor}` | ||||
|       return searchQuery | ||||
|     }, | ||||
|     submitSearch() { | ||||
|       if (!this.searchTitle) { | ||||
|         this.$toast.warning('Search title is required') | ||||
|         return | ||||
|       } | ||||
|       this.runSearch() | ||||
|     }, | ||||
|     async runSearch() { | ||||
|       if (this.lastSearch === this.search) return | ||||
|       console.log('Search', this.lastSearch, this.search) | ||||
| 
 | ||||
|       var searchQuery = this.getSearchQuery() | ||||
|       if (this.lastSearch === searchQuery) return | ||||
|       this.searchResults = [] | ||||
|       this.isProcessing = true | ||||
|       this.lastSearch = this.search | ||||
|       var results = await this.$axios.$get(`/api/find/search?title=${this.search}`).catch((error) => { | ||||
|       this.lastSearch = searchQuery | ||||
|       var results = await this.$axios.$get(`/api/find/search?${searchQuery}`).catch((error) => { | ||||
|         console.error('Failed', error) | ||||
|         return [] | ||||
|       }) | ||||
|       results = results.filter((res) => { | ||||
|         return !!res.title | ||||
|       }) | ||||
|       console.log('Got results', results) | ||||
|       this.searchResults = results | ||||
|       this.isProcessing = false | ||||
|     }, | ||||
|     init() { | ||||
|       if (!this.audiobook.book || !this.audiobook.book.title) { | ||||
|         this.search = null | ||||
|         this.searchTitle = null | ||||
|         return | ||||
|       } | ||||
|       if (this.searchResults.length) { | ||||
|         console.log('Already hav ereuslts', this.searchResults, this.lastSearch) | ||||
|       } | ||||
|       this.search = this.audiobook.book.title | ||||
|       this.searchTitle = this.audiobook.book.title | ||||
|       this.searchAuthor = this.audiobook.book.author || '' | ||||
|       this.runSearch() | ||||
|     }, | ||||
|     async selectMatch(match) { | ||||
| @ -136,4 +127,10 @@ export default { | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| </script> | ||||
| 
 | ||||
| <style> | ||||
| .matchListWrapper { | ||||
|   height: calc(100% - 80px); | ||||
| } | ||||
| </style> | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "0.9.54", | ||||
|   "version": "0.9.6", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "0.9.54", | ||||
|   "version": "0.9.6", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks.", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -23,7 +23,6 @@ Title can start with the publish year like so: | ||||
| 
 | ||||
| * Adding new audiobooks require pressing Scan button again (on settings page) | ||||
| * Matching is all manual now and only using 1 source (openlibrary) | ||||
| * Need to add cover selection from match results | ||||
| * Support different views to see more details of each audiobook | ||||
| * Then comes the mobile app.. | ||||
| 
 | ||||
|  | ||||
| @ -14,8 +14,10 @@ class ApiController { | ||||
|   } | ||||
| 
 | ||||
|   init() { | ||||
|     this.router.get('/find/covers', this.findCovers.bind(this)) | ||||
|     this.router.get('/find/:method', this.find.bind(this)) | ||||
| 
 | ||||
| 
 | ||||
|     this.router.get('/audiobooks', this.getAudiobooks.bind(this)) | ||||
|     this.router.get('/audiobook/:id', this.getAudiobook.bind(this)) | ||||
|     this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this)) | ||||
| @ -36,6 +38,11 @@ class ApiController { | ||||
|     this.scanner.find(req, res) | ||||
|   } | ||||
| 
 | ||||
|   findCovers(req, res) { | ||||
|     console.log('Find covers', req.query) | ||||
|     this.scanner.findCovers(req, res) | ||||
|   } | ||||
| 
 | ||||
|   async getMetadata(req, res) { | ||||
|     var metadata = await this.scanner.fetchMetadata(req.params.id, req.params.trackIndex) | ||||
|     res.json(metadata) | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| const OpenLibrary = require('./providers/OpenLibrary') | ||||
| const LibGen = require('./providers/LibGen') | ||||
| const Logger = require('./Logger') | ||||
| const { levenshteinDistance } = require('./utils/index') | ||||
| 
 | ||||
| class BookFinder { | ||||
|   constructor() { | ||||
| @ -15,19 +17,142 @@ class BookFinder { | ||||
|     return book | ||||
|   } | ||||
| 
 | ||||
|   async search(query, provider = 'openlibrary') { | ||||
|     var books = null | ||||
|   stripSubtitle(title) { | ||||
|     if (title.includes(':')) { | ||||
|       return title.split(':')[0].trim() | ||||
|     } else if (title.includes(' - ')) { | ||||
|       return title.split(' - ')[0].trim() | ||||
|     } | ||||
|     return title | ||||
|   } | ||||
| 
 | ||||
|   cleanTitleForCompares(title) { | ||||
|     // Remove subtitle if there (i.e. "Cool Book: Coolest Ever" becomes "Cool Book")
 | ||||
|     var stripped = this.stripSubtitle(title) | ||||
| 
 | ||||
|     // Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game")
 | ||||
|     var cleaned = stripped.replace(/ *\([^)]*\) */g, "") | ||||
| 
 | ||||
|     // Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
 | ||||
|     cleaned = cleaned.replace(/'/g, '') | ||||
|     return cleaned.toLowerCase() | ||||
|   } | ||||
| 
 | ||||
|   filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) { | ||||
|     var searchTitle = this.cleanTitleForCompares(title) | ||||
|     return books.map(b => { | ||||
|       b.cleanedTitle = this.cleanTitleForCompares(b.title) | ||||
|       b.titleDistance = levenshteinDistance(b.cleanedTitle, title) | ||||
|       if (author) { | ||||
|         b.authorDistance = levenshteinDistance(b.author || '', author) | ||||
|       } | ||||
|       b.totalDistance = b.titleDistance + (b.authorDistance || 0) | ||||
|       b.totalPossibleDistance = b.title.length | ||||
| 
 | ||||
|       if (b.cleanedTitle.includes(searchTitle) && searchTitle.length > 4) { | ||||
|         b.includesSearch = searchTitle | ||||
|       } else if (b.title.includes(searchTitle) && searchTitle.length > 4) { | ||||
|         b.includesSearch = searchTitle | ||||
|       } | ||||
| 
 | ||||
|       if (author && b.author) b.totalPossibleDistance += b.author.length | ||||
| 
 | ||||
|       return b | ||||
|     }).filter(b => { | ||||
|       if (b.includesSearch) { // If search was found in result title exactly then skip over leven distance check
 | ||||
|         Logger.debug(`Exact search was found inside title ${b.cleanedTitle}/${b.includesSearch}`) | ||||
|       } else if (b.titleDistance > maxTitleDistance) { | ||||
|         Logger.debug(`Filtering out search result title distance = ${b.titleDistance}: "${b.cleanedTitle}"/"${searchTitle}"`) | ||||
|         return false | ||||
|       } | ||||
| 
 | ||||
|       if (author && b.authorDistance > maxAuthorDistance) { | ||||
|         Logger.debug(`Filtering out search result "${b.title}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`) | ||||
|         return false | ||||
|       } | ||||
| 
 | ||||
|       if (b.totalPossibleDistance < 4 && b.totalDistance > 0) return false | ||||
|       return true | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) { | ||||
|     var books = await this.libGen.search(title) | ||||
|     Logger.info(`LibGen Book Search Results: ${books.length || 0}`) | ||||
|     if (books.errorCode) { | ||||
|       Logger.error(`LibGen Search Error ${books.errorCode}`) | ||||
|       return [] | ||||
|     } | ||||
|     var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) | ||||
|     if (!booksFiltered.length && books.length) { | ||||
|       Logger.info(`Search has ${books.length} matches, but no close title matches`) | ||||
|     } | ||||
|     return booksFiltered | ||||
|   } | ||||
| 
 | ||||
|   async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) { | ||||
|     var books = await this.openLibrary.searchTitle(title) | ||||
|     Logger.info(`OpenLib Book Search Results: ${books.length || 0}`) | ||||
|     if (books.errorCode) { | ||||
|       Logger.error(`OpenLib Search Error ${books.errorCode}`) | ||||
|       return [] | ||||
|     } | ||||
|     var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) | ||||
|     if (!booksFiltered.length && books.length) { | ||||
|       Logger.info(`Search has ${books.length} matches, but no close title matches`) | ||||
|     } | ||||
|     return booksFiltered | ||||
|   } | ||||
| 
 | ||||
|   async search(provider, title, author, options = {}) { | ||||
|     var books = [] | ||||
|     var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4 | ||||
|     var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4 | ||||
|     Logger.info(`Book Search, title: "${title}", author: "${author}", provider: ${provider}`) | ||||
| 
 | ||||
|     if (provider === 'libgen') { | ||||
|       books = await this.libGen.search(query) | ||||
|       return books | ||||
|       books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) | ||||
|     } else if (provider === 'openlibrary') { | ||||
|       books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) | ||||
|     } else if (provider === 'all') { | ||||
|       var lbBooks = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) | ||||
|       var olBooks = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) | ||||
|       books = books.concat(lbBooks, olBooks) | ||||
|     } else { | ||||
|       var olBooks = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) | ||||
|       var hasCloseMatch = olBooks.find(b => (b.totalDistance < 4 && b.totalPossibleDistance > 4)) | ||||
|       if (hasCloseMatch) { | ||||
|         books = olBooks | ||||
|       } else { | ||||
|         Logger.info(`Book Search, LibGen has no close matches - get openlib results also`) | ||||
|         var lbBooks = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance) | ||||
|         books = books.concat(lbBooks) | ||||
|       } | ||||
| 
 | ||||
|       if (!books.length && author) { | ||||
|         Logger.info(`Book Search, no matches for title and author.. check title only`) | ||||
|         return this.search(provider, title, null, options) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     books = await this.openLibrary.search(query) | ||||
|     if (books.errorCode) { | ||||
|       console.error('Books not found') | ||||
|     } | ||||
|     return books | ||||
|     return books.sort((a, b) => { | ||||
|       return a.totalDistance - b.totalDistance | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   async findCovers(provider, title, author, options = {}) { | ||||
|     var searchResults = await this.search(provider, title, author, options) | ||||
|     console.log('Find Covers search results', searchResults) | ||||
|     var covers = [] | ||||
|     searchResults.forEach((result) => { | ||||
|       if (result.covers && result.covers.length) { | ||||
|         covers = covers.concat(result.covers) | ||||
|       } | ||||
|       if (result.cover) { | ||||
|         covers.push(result.cover) | ||||
|       } | ||||
|     }) | ||||
|     return covers | ||||
|   } | ||||
| } | ||||
| module.exports = BookFinder | ||||
| @ -77,14 +77,18 @@ class Scanner { | ||||
|     var result = null | ||||
| 
 | ||||
|     if (method === 'isbn') { | ||||
|       console.log('Search', query, 'via ISBN') | ||||
|       result = await this.bookFinder.findByISBN(query) | ||||
|     } else if (method === 'search') { | ||||
|       console.log('Search', query, 'via query') | ||||
|       result = await this.bookFinder.search(query) | ||||
|       result = await this.bookFinder.search(query.provider, query.title, query.author || null) | ||||
|     } | ||||
| 
 | ||||
|     res.json(result) | ||||
|   } | ||||
| 
 | ||||
|   async findCovers(req, res) { | ||||
|     var query = req.query | ||||
|     var result = await this.bookFinder.findCovers(query.provider, query.title, query.author || null) | ||||
|     res.json(result) | ||||
|   } | ||||
| } | ||||
| module.exports = Scanner | ||||
| @ -10,28 +10,40 @@ class LibGen { | ||||
|     console.log(`${this.mirror} is currently fastest`) | ||||
|   } | ||||
| 
 | ||||
|   async search(query) { | ||||
|   async search(queryTitle) { | ||||
|     if (!this.mirror) { | ||||
|       await this.init() | ||||
|     } | ||||
|     queryTitle = queryTitle.replace(/'/g, '') | ||||
|     var options = { | ||||
|       mirror: this.mirror, | ||||
|       query: query, | ||||
|       query: queryTitle, | ||||
|       search_in: 'title' | ||||
|     } | ||||
|     var httpsMirror = this.mirror | ||||
|     if (httpsMirror.startsWith('http:')) { | ||||
|       httpsMirror = httpsMirror.replace('http:', 'https:') | ||||
|     } | ||||
|     // console.log('LibGen Search Options', options)
 | ||||
|     try { | ||||
|       const data = await libgen.search(options) | ||||
|       let n = data.length | ||||
|       console.log(`${n} results for "${options.query}"`) | ||||
|       // console.log(`${n} results for "${options.query}"`)
 | ||||
|       var cleanedResults = [] | ||||
|       while (n--) { | ||||
|         console.log(''); | ||||
|         console.log('Title: ' + data[n].title) | ||||
|         console.log('Author: ' + data[n].author) | ||||
|         console.log('Download: ' + | ||||
|           'http://gen.lib.rus.ec/book/index.php?md5=' + | ||||
|           data[n].md5.toLowerCase()) | ||||
|         var resultObj = { | ||||
|           id: data[n].id, | ||||
|           title: data[n].title, | ||||
|           author: data[n].author, | ||||
|           publisher: data[n].publisher, | ||||
|           description: data[n].descr, | ||||
|           cover: `${httpsMirror}/covers/${data[n].coverurl}`, | ||||
|           year: data[n].year | ||||
|         } | ||||
|         if (!resultObj.title) continue; | ||||
|         cleanedResults.push(resultObj) | ||||
|       } | ||||
|       return data | ||||
|       return cleanedResults | ||||
|     } catch (err) { | ||||
|       console.error(err) | ||||
|       return { | ||||
|  | ||||
| @ -50,7 +50,7 @@ class OpenLibrary { | ||||
|     return { | ||||
|       title: doc.title, | ||||
|       author: doc.author_name ? doc.author_name.join(', ') : null, | ||||
|       first_publish_year: doc.first_publish_year, | ||||
|       year: doc.first_publish_year, | ||||
|       edition: doc.cover_edition_key, | ||||
|       cover: doc.cover_edition_key ? `https://covers.openlibrary.org/b/OLID/${doc.cover_edition_key}-L.jpg` : null, | ||||
|       ...worksData | ||||
| @ -68,5 +68,17 @@ class OpenLibrary { | ||||
|     var searchDocs = await Promise.all(lookupData.docs.map(d => this.cleanSearchDoc(d))) | ||||
|     return searchDocs | ||||
|   } | ||||
| 
 | ||||
|   async searchTitle(title) { | ||||
|     title = title.replace(/'/g, '') | ||||
|     var lookupData = await this.get(`/search.json?title=${title}`) | ||||
|     if (!lookupData) { | ||||
|       return { | ||||
|         errorCode: 404 | ||||
|       } | ||||
|     } | ||||
|     var searchDocs = await Promise.all(lookupData.docs.map(d => this.cleanSearchDoc(d))) | ||||
|     return searchDocs | ||||
|   } | ||||
| } | ||||
| module.exports = OpenLibrary | ||||
							
								
								
									
										26
									
								
								server/utils/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								server/utils/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| const levenshteinDistance = (str1, str2, caseSensitive = false) => { | ||||
|   if (!caseSensitive) { | ||||
|     str1 = str1.toLowerCase() | ||||
|     str2 = str2.toLowerCase() | ||||
|   } | ||||
|   const track = Array(str2.length + 1).fill(null).map(() => | ||||
|     Array(str1.length + 1).fill(null)); | ||||
|   for (let i = 0; i <= str1.length; i += 1) { | ||||
|     track[0][i] = i; | ||||
|   } | ||||
|   for (let j = 0; j <= str2.length; j += 1) { | ||||
|     track[j][0] = j; | ||||
|   } | ||||
|   for (let j = 1; j <= str2.length; j += 1) { | ||||
|     for (let i = 1; i <= str1.length; i += 1) { | ||||
|       const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1; | ||||
|       track[j][i] = Math.min( | ||||
|         track[j][i - 1] + 1, // deletion
 | ||||
|         track[j - 1][i] + 1, // insertion
 | ||||
|         track[j - 1][i - 1] + indicator, // substitution
 | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|   return track[str2.length][str1.length]; | ||||
| } | ||||
| module.exports.levenshteinDistance = levenshteinDistance | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user