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
							
								
									be7e2576f1
								
							
						
					
					
						commit
						30700c1eb0
					
				@ -1,5 +1,5 @@
 | 
				
			|||||||
<template>
 | 
					<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" />
 | 
					    <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' }">
 | 
					    <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 (newVal) {
 | 
				
			||||||
          if (this.audiobook && this.audiobook.id === this.selectedAudiobookId) return
 | 
					          if (this.audiobook && this.audiobook.id === this.selectedAudiobookId) return
 | 
				
			||||||
          this.audiobook = null
 | 
					          this.audiobook = null
 | 
				
			||||||
          this.fetchFull()
 | 
					          this.init()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this.$store.commit('audiobooks/removeListener', 'edit-modal')
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -72,6 +74,13 @@ export default {
 | 
				
			|||||||
    selectTab(tab) {
 | 
					    selectTab(tab) {
 | 
				
			||||||
      this.selectedTab = 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() {
 | 
					    async fetchFull() {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        this.audiobook = await this.$axios.$get(`/api/audiobook/${this.selectedAudiobookId}`)
 | 
					        this.audiobook = await this.$axios.$get(`/api/audiobook/${this.selectedAudiobookId}`)
 | 
				
			||||||
 | 
				
			|||||||
@ -2,13 +2,35 @@
 | 
				
			|||||||
  <div class="w-full h-full">
 | 
					  <div class="w-full h-full">
 | 
				
			||||||
    <div class="flex">
 | 
					    <div class="flex">
 | 
				
			||||||
      <cards-book-cover :audiobook="audiobook" />
 | 
					      <cards-book-cover :audiobook="audiobook" />
 | 
				
			||||||
      <div class="flex-grow px-8">
 | 
					      <div class="flex-grow pl-6 pr-2">
 | 
				
			||||||
        <form @submit.prevent="submitForm">
 | 
					        <form @submit.prevent="submitForm">
 | 
				
			||||||
          <div class="flex items-center">
 | 
					          <div class="flex items-center">
 | 
				
			||||||
            <ui-text-input-with-label v-model="imageUrl" label="Cover Image URL" />
 | 
					            <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>
 | 
					          </div>
 | 
				
			||||||
        </form>
 | 
					        </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>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@ -25,7 +47,11 @@ export default {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      imageUrl: null
 | 
					      searchTitle: null,
 | 
				
			||||||
 | 
					      searchAuthor: null,
 | 
				
			||||||
 | 
					      imageUrl: null,
 | 
				
			||||||
 | 
					      coversFound: [],
 | 
				
			||||||
 | 
					      hasSearched: false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  watch: {
 | 
					  watch: {
 | 
				
			||||||
@ -51,14 +77,22 @@ export default {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    init() {
 | 
					    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.imageUrl = this.book.cover || ''
 | 
				
			||||||
 | 
					      this.searchTitle = this.book.title || ''
 | 
				
			||||||
 | 
					      this.searchAuthor = this.book.author || ''
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async submitForm() {
 | 
					    submitForm() {
 | 
				
			||||||
      console.log('Submit form', this.details)
 | 
					      this.updateCover(this.imageUrl)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    async updateCover(cover) {
 | 
				
			||||||
      this.isProcessing = true
 | 
					      this.isProcessing = true
 | 
				
			||||||
      const updatePayload = {
 | 
					      const updatePayload = {
 | 
				
			||||||
        book: {
 | 
					        book: {
 | 
				
			||||||
          cover: this.imageUrl
 | 
					          cover: cover
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      var updatedAudiobook = await this.$axios.$patch(`/api/audiobook/${this.audiobook.id}`, updatePayload).catch((error) => {
 | 
					      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.$toast.success('Update Successful')
 | 
				
			||||||
        this.$emit('close')
 | 
					        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>
 | 
					<template>
 | 
				
			||||||
  <div class="w-full h-full overflow-hidden">
 | 
					  <div class="w-full h-full overflow-hidden">
 | 
				
			||||||
    <div class="flex items-center mb-4">
 | 
					 | 
				
			||||||
      <div class="w-72">
 | 
					 | 
				
			||||||
    <form @submit.prevent="submitSearch">
 | 
					    <form @submit.prevent="submitSearch">
 | 
				
			||||||
          <ui-text-input-with-label v-model="search" label="Search Title" placeholder="Search" :disabled="processing" />
 | 
					      <div class="flex items-center justify-start -mx-1 h-20">
 | 
				
			||||||
        </form>
 | 
					        <div class="w-72 px-1">
 | 
				
			||||||
 | 
					          <ui-text-input-with-label v-model="searchTitle" label="Search Title" placeholder="Search" :disabled="processing" />
 | 
				
			||||||
        </div>
 | 
					        </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 class="flex-grow" />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					    </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">
 | 
				
			||||||
      <p>Loading...</p>
 | 
					      <p>Loading...</p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div v-show="!processing && !searchResults.length" class="flex h-full items-center justify-center">
 | 
					    <div v-show="!processing && !searchResults.length" class="flex h-full items-center justify-center">
 | 
				
			||||||
      <p>No Results</p>
 | 
					      <p>No Results</p>
 | 
				
			||||||
    </div>
 | 
					    </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">
 | 
					      <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)">
 | 
					        <cards-book-match-card :key="index" :book="res" @select="selectMatch" />
 | 
				
			||||||
          <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>
 | 
					 | 
				
			||||||
      </template>
 | 
					      </template>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@ -53,8 +37,10 @@ export default {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
  data() {
 | 
					  data() {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      search: null,
 | 
					      searchTitle: null,
 | 
				
			||||||
 | 
					      searchAuthor: null,
 | 
				
			||||||
      lastSearch: null,
 | 
					      lastSearch: null,
 | 
				
			||||||
 | 
					      provider: 'best',
 | 
				
			||||||
      searchResults: []
 | 
					      searchResults: []
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
@ -77,36 +63,41 @@ export default {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
 | 
					    getSearchQuery() {
 | 
				
			||||||
 | 
					      var searchQuery = `provider=${this.provider}&title=${this.searchTitle}`
 | 
				
			||||||
 | 
					      if (this.searchAuthor) searchQuery += `&author=${this.searchAuthor}`
 | 
				
			||||||
 | 
					      return searchQuery
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    submitSearch() {
 | 
					    submitSearch() {
 | 
				
			||||||
 | 
					      if (!this.searchTitle) {
 | 
				
			||||||
 | 
					        this.$toast.warning('Search title is required')
 | 
				
			||||||
 | 
					        return
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      this.runSearch()
 | 
					      this.runSearch()
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async runSearch() {
 | 
					    async runSearch() {
 | 
				
			||||||
      if (this.lastSearch === this.search) return
 | 
					      var searchQuery = this.getSearchQuery()
 | 
				
			||||||
      console.log('Search', this.lastSearch, this.search)
 | 
					      if (this.lastSearch === searchQuery) return
 | 
				
			||||||
 | 
					 | 
				
			||||||
      this.searchResults = []
 | 
					      this.searchResults = []
 | 
				
			||||||
      this.isProcessing = true
 | 
					      this.isProcessing = true
 | 
				
			||||||
      this.lastSearch = this.search
 | 
					      this.lastSearch = searchQuery
 | 
				
			||||||
      var results = await this.$axios.$get(`/api/find/search?title=${this.search}`).catch((error) => {
 | 
					      var results = await this.$axios.$get(`/api/find/search?${searchQuery}`).catch((error) => {
 | 
				
			||||||
        console.error('Failed', error)
 | 
					        console.error('Failed', error)
 | 
				
			||||||
        return []
 | 
					        return []
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      results = results.filter((res) => {
 | 
					      results = results.filter((res) => {
 | 
				
			||||||
        return !!res.title
 | 
					        return !!res.title
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      console.log('Got results', results)
 | 
					 | 
				
			||||||
      this.searchResults = results
 | 
					      this.searchResults = results
 | 
				
			||||||
      this.isProcessing = false
 | 
					      this.isProcessing = false
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    init() {
 | 
					    init() {
 | 
				
			||||||
      if (!this.audiobook.book || !this.audiobook.book.title) {
 | 
					      if (!this.audiobook.book || !this.audiobook.book.title) {
 | 
				
			||||||
        this.search = null
 | 
					        this.searchTitle = null
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (this.searchResults.length) {
 | 
					      this.searchTitle = this.audiobook.book.title
 | 
				
			||||||
        console.log('Already hav ereuslts', this.searchResults, this.lastSearch)
 | 
					      this.searchAuthor = this.audiobook.book.author || ''
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this.search = this.audiobook.book.title
 | 
					 | 
				
			||||||
      this.runSearch()
 | 
					      this.runSearch()
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    async selectMatch(match) {
 | 
					    async selectMatch(match) {
 | 
				
			||||||
@ -137,3 +128,9 @@ export default {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					.matchListWrapper {
 | 
				
			||||||
 | 
					  height: calc(100% - 80px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "audiobookshelf-client",
 | 
					  "name": "audiobookshelf-client",
 | 
				
			||||||
  "version": "0.9.54",
 | 
					  "version": "0.9.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": "0.9.54",
 | 
					  "version": "0.9.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": {
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,6 @@ Title can start with the publish year like so:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
* Adding new audiobooks require pressing Scan button again (on settings page)
 | 
					* Adding new audiobooks require pressing Scan button again (on settings page)
 | 
				
			||||||
* Matching is all manual now and only using 1 source (openlibrary)
 | 
					* 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
 | 
					* Support different views to see more details of each audiobook
 | 
				
			||||||
* Then comes the mobile app..
 | 
					* Then comes the mobile app..
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -14,8 +14,10 @@ class ApiController {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  init() {
 | 
					  init() {
 | 
				
			||||||
 | 
					    this.router.get('/find/covers', this.findCovers.bind(this))
 | 
				
			||||||
    this.router.get('/find/:method', this.find.bind(this))
 | 
					    this.router.get('/find/:method', this.find.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.router.get('/audiobooks', this.getAudiobooks.bind(this))
 | 
					    this.router.get('/audiobooks', this.getAudiobooks.bind(this))
 | 
				
			||||||
    this.router.get('/audiobook/:id', this.getAudiobook.bind(this))
 | 
					    this.router.get('/audiobook/:id', this.getAudiobook.bind(this))
 | 
				
			||||||
    this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this))
 | 
					    this.router.delete('/audiobook/:id', this.deleteAudiobook.bind(this))
 | 
				
			||||||
@ -36,6 +38,11 @@ class ApiController {
 | 
				
			|||||||
    this.scanner.find(req, res)
 | 
					    this.scanner.find(req, res)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  findCovers(req, res) {
 | 
				
			||||||
 | 
					    console.log('Find covers', req.query)
 | 
				
			||||||
 | 
					    this.scanner.findCovers(req, res)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getMetadata(req, res) {
 | 
					  async getMetadata(req, res) {
 | 
				
			||||||
    var metadata = await this.scanner.fetchMetadata(req.params.id, req.params.trackIndex)
 | 
					    var metadata = await this.scanner.fetchMetadata(req.params.id, req.params.trackIndex)
 | 
				
			||||||
    res.json(metadata)
 | 
					    res.json(metadata)
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,7 @@
 | 
				
			|||||||
const OpenLibrary = require('./providers/OpenLibrary')
 | 
					const OpenLibrary = require('./providers/OpenLibrary')
 | 
				
			||||||
const LibGen = require('./providers/LibGen')
 | 
					const LibGen = require('./providers/LibGen')
 | 
				
			||||||
 | 
					const Logger = require('./Logger')
 | 
				
			||||||
 | 
					const { levenshteinDistance } = require('./utils/index')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BookFinder {
 | 
					class BookFinder {
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
@ -15,19 +17,142 @@ class BookFinder {
 | 
				
			|||||||
    return book
 | 
					    return book
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async search(query, provider = 'openlibrary') {
 | 
					  stripSubtitle(title) {
 | 
				
			||||||
    var books = null
 | 
					    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') {
 | 
					    if (provider === 'libgen') {
 | 
				
			||||||
      books = await this.libGen.search(query)
 | 
					      books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
 | 
				
			||||||
      return books
 | 
					    } 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)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    books = await this.openLibrary.search(query)
 | 
					      if (!books.length && author) {
 | 
				
			||||||
    if (books.errorCode) {
 | 
					        Logger.info(`Book Search, no matches for title and author.. check title only`)
 | 
				
			||||||
      console.error('Books not found')
 | 
					        return this.search(provider, title, null, options)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    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
 | 
					module.exports = BookFinder
 | 
				
			||||||
@ -77,14 +77,18 @@ class Scanner {
 | 
				
			|||||||
    var result = null
 | 
					    var result = null
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (method === 'isbn') {
 | 
					    if (method === 'isbn') {
 | 
				
			||||||
      console.log('Search', query, 'via ISBN')
 | 
					 | 
				
			||||||
      result = await this.bookFinder.findByISBN(query)
 | 
					      result = await this.bookFinder.findByISBN(query)
 | 
				
			||||||
    } else if (method === 'search') {
 | 
					    } else if (method === 'search') {
 | 
				
			||||||
      console.log('Search', query, 'via query')
 | 
					      result = await this.bookFinder.search(query.provider, query.title, query.author || null)
 | 
				
			||||||
      result = await this.bookFinder.search(query)
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.json(result)
 | 
					    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
 | 
					module.exports = Scanner
 | 
				
			||||||
@ -10,28 +10,40 @@ class LibGen {
 | 
				
			|||||||
    console.log(`${this.mirror} is currently fastest`)
 | 
					    console.log(`${this.mirror} is currently fastest`)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async search(query) {
 | 
					  async search(queryTitle) {
 | 
				
			||||||
    if (!this.mirror) {
 | 
					    if (!this.mirror) {
 | 
				
			||||||
      await this.init()
 | 
					      await this.init()
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    queryTitle = queryTitle.replace(/'/g, '')
 | 
				
			||||||
    var options = {
 | 
					    var options = {
 | 
				
			||||||
      mirror: this.mirror,
 | 
					      mirror: this.mirror,
 | 
				
			||||||
      query: query,
 | 
					      query: queryTitle,
 | 
				
			||||||
      search_in: 'title'
 | 
					      search_in: 'title'
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    var httpsMirror = this.mirror
 | 
				
			||||||
 | 
					    if (httpsMirror.startsWith('http:')) {
 | 
				
			||||||
 | 
					      httpsMirror = httpsMirror.replace('http:', 'https:')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // console.log('LibGen Search Options', options)
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const data = await libgen.search(options)
 | 
					      const data = await libgen.search(options)
 | 
				
			||||||
      let n = data.length
 | 
					      let n = data.length
 | 
				
			||||||
      console.log(`${n} results for "${options.query}"`)
 | 
					      // console.log(`${n} results for "${options.query}"`)
 | 
				
			||||||
 | 
					      var cleanedResults = []
 | 
				
			||||||
      while (n--) {
 | 
					      while (n--) {
 | 
				
			||||||
        console.log('');
 | 
					        var resultObj = {
 | 
				
			||||||
        console.log('Title: ' + data[n].title)
 | 
					          id: data[n].id,
 | 
				
			||||||
        console.log('Author: ' + data[n].author)
 | 
					          title: data[n].title,
 | 
				
			||||||
        console.log('Download: ' +
 | 
					          author: data[n].author,
 | 
				
			||||||
          'http://gen.lib.rus.ec/book/index.php?md5=' +
 | 
					          publisher: data[n].publisher,
 | 
				
			||||||
          data[n].md5.toLowerCase())
 | 
					          description: data[n].descr,
 | 
				
			||||||
 | 
					          cover: `${httpsMirror}/covers/${data[n].coverurl}`,
 | 
				
			||||||
 | 
					          year: data[n].year
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      return data
 | 
					        if (!resultObj.title) continue;
 | 
				
			||||||
 | 
					        cleanedResults.push(resultObj)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return cleanedResults
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      console.error(err)
 | 
					      console.error(err)
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
 | 
				
			|||||||
@ -50,7 +50,7 @@ class OpenLibrary {
 | 
				
			|||||||
    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,
 | 
				
			||||||
      first_publish_year: doc.first_publish_year,
 | 
					      year: doc.first_publish_year,
 | 
				
			||||||
      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
 | 
				
			||||||
@ -68,5 +68,17 @@ class OpenLibrary {
 | 
				
			|||||||
    var searchDocs = await Promise.all(lookupData.docs.map(d => this.cleanSearchDoc(d)))
 | 
					    var searchDocs = await Promise.all(lookupData.docs.map(d => this.cleanSearchDoc(d)))
 | 
				
			||||||
    return searchDocs
 | 
					    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
 | 
					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