mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +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>
|
||||
<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>
|
||||
<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>
|
||||
</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) {
|
||||
@ -137,3 +128,9 @@ export default {
|
||||
}
|
||||
}
|
||||
</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)
|
||||
}
|
||||
|
||||
books = await this.openLibrary.search(query)
|
||||
if (books.errorCode) {
|
||||
console.error('Books not found')
|
||||
if (!books.length && author) {
|
||||
Logger.info(`Book Search, no matches for title and author.. check title only`)
|
||||
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
|
@ -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
|
||||
}
|
||||
return data
|
||||
if (!resultObj.title) continue;
|
||||
cleanedResults.push(resultObj)
|
||||
}
|
||||
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