mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-02-04 12:29:34 +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">
|
<form @submit.prevent="submitSearch">
|
||||||
<div class="w-72">
|
<div class="flex items-center justify-start -mx-1 h-20">
|
||||||
<form @submit.prevent="submitSearch">
|
<div class="w-72 px-1">
|
||||||
<ui-text-input-with-label v-model="search" label="Search Title" placeholder="Search" :disabled="processing" />
|
<ui-text-input-with-label v-model="searchTitle" label="Search Title" placeholder="Search" :disabled="processing" />
|
||||||
</form>
|
</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>
|
||||||
<div class="flex-grow" />
|
</form>
|
||||||
</div>
|
|
||||||
<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) {
|
||||||
@ -136,4 +127,10 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
return books.sort((a, b) => {
|
||||||
if (books.errorCode) {
|
return a.totalDistance - b.totalDistance
|
||||||
console.error('Books not found')
|
})
|
||||||
}
|
}
|
||||||
return books
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if (!resultObj.title) continue;
|
||||||
|
cleanedResults.push(resultObj)
|
||||||
}
|
}
|
||||||
return data
|
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