2022-03-06 23:32:04 +01:00
|
|
|
const OpenLibrary = require('../providers/OpenLibrary')
|
|
|
|
const GoogleBooks = require('../providers/GoogleBooks')
|
|
|
|
const Audible = require('../providers/Audible')
|
2022-03-07 00:26:35 +01:00
|
|
|
const iTunes = require('../providers/iTunes')
|
2022-05-11 00:03:41 +02:00
|
|
|
const Audnexus = require('../providers/Audnexus')
|
2023-02-06 22:25:18 +01:00
|
|
|
const FantLab = require('../providers/FantLab')
|
2023-04-20 07:13:52 +02:00
|
|
|
const AudiobookCovers = require('../providers/AudiobookCovers')
|
2022-03-06 23:32:04 +01:00
|
|
|
const Logger = require('../Logger')
|
|
|
|
const { levenshteinDistance } = require('../utils/index')
|
2021-08-18 00:01:11 +02:00
|
|
|
|
|
|
|
class BookFinder {
|
|
|
|
constructor() {
|
|
|
|
this.openLibrary = new OpenLibrary()
|
2021-10-28 21:41:42 +02:00
|
|
|
this.googleBooks = new GoogleBooks()
|
2021-11-21 19:59:32 +01:00
|
|
|
this.audible = new Audible()
|
2022-03-07 00:26:35 +01:00
|
|
|
this.iTunesApi = new iTunes()
|
2022-05-11 00:03:41 +02:00
|
|
|
this.audnexus = new Audnexus()
|
2023-02-06 22:25:18 +01:00
|
|
|
this.fantLab = new FantLab()
|
2023-04-20 07:13:52 +02:00
|
|
|
this.audiobookCovers = new AudiobookCovers()
|
2021-10-06 04:10:49 +02:00
|
|
|
|
2023-05-14 20:43:20 +02:00
|
|
|
this.providers = ['google', 'itunes', 'openlibrary', 'fantlab', 'audiobookcovers', 'audible', 'audible.ca', 'audible.uk', 'audible.au', 'audible.fr', 'audible.de', 'audible.jp', 'audible.it', 'audible.in', 'audible.es']
|
|
|
|
|
2021-10-06 04:10:49 +02:00
|
|
|
this.verbose = false
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async findByISBN(isbn) {
|
|
|
|
var book = await this.openLibrary.isbnLookup(isbn)
|
|
|
|
if (book.errorCode) {
|
2021-08-26 02:15:00 +02:00
|
|
|
Logger.error('Book not found')
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
|
|
|
return book
|
|
|
|
}
|
|
|
|
|
2021-08-21 16:15:44 +02:00
|
|
|
stripSubtitle(title) {
|
|
|
|
if (title.includes(':')) {
|
|
|
|
return title.split(':')[0].trim()
|
|
|
|
} else if (title.includes(' - ')) {
|
|
|
|
return title.split(' - ')[0].trim()
|
|
|
|
}
|
|
|
|
return title
|
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
|
2021-08-25 03:24:40 +02:00
|
|
|
replaceAccentedChars(str) {
|
|
|
|
try {
|
|
|
|
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
|
|
|
|
} catch (error) {
|
|
|
|
Logger.error('[BookFinder] str normalize error', error)
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-21 16:15:44 +02:00
|
|
|
cleanTitleForCompares(title) {
|
2021-08-25 03:24:40 +02:00
|
|
|
if (!title) return ''
|
2021-08-21 16:15:44 +02:00
|
|
|
// Remove subtitle if there (i.e. "Cool Book: Coolest Ever" becomes "Cool Book")
|
2023-09-22 23:03:41 +02:00
|
|
|
let stripped = this.stripSubtitle(title)
|
2021-08-21 16:15:44 +02:00
|
|
|
|
|
|
|
// Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game")
|
2023-09-22 23:03:41 +02:00
|
|
|
let cleaned = stripped.replace(/ *\([^)]*\) */g, "")
|
2021-08-21 16:15:44 +02:00
|
|
|
|
|
|
|
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
|
|
|
|
cleaned = cleaned.replace(/'/g, '')
|
2023-10-03 21:43:37 +02:00
|
|
|
return this.replaceAccentedChars(cleaned).toLowerCase()
|
2021-08-25 03:24:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
cleanAuthorForCompares(author) {
|
|
|
|
if (!author) return ''
|
2023-10-05 16:50:16 +02:00
|
|
|
let cleanAuthor = this.replaceAccentedChars(author).toLowerCase()
|
|
|
|
// separate initials
|
|
|
|
cleanAuthor = cleanAuthor.replace(/([a-z])\.([a-z])/g, '$1. $2')
|
|
|
|
// remove middle initials
|
|
|
|
cleanAuthor = cleanAuthor.replace(/(?<=\w\w)(\s+[a-z]\.?)+(?=\s+\w\w)/g, '')
|
|
|
|
return cleanAuthor
|
2021-08-21 16:15:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) {
|
|
|
|
var searchTitle = this.cleanTitleForCompares(title)
|
2021-08-25 03:24:40 +02:00
|
|
|
var searchAuthor = this.cleanAuthorForCompares(author)
|
2021-08-21 16:15:44 +02:00
|
|
|
return books.map(b => {
|
|
|
|
b.cleanedTitle = this.cleanTitleForCompares(b.title)
|
|
|
|
b.titleDistance = levenshteinDistance(b.cleanedTitle, title)
|
2021-08-26 02:15:00 +02:00
|
|
|
|
|
|
|
// Total length of search (title or both title & author)
|
|
|
|
b.totalPossibleDistance = b.title.length
|
|
|
|
|
2021-08-21 16:15:44 +02:00
|
|
|
if (author) {
|
2021-08-25 03:24:40 +02:00
|
|
|
if (!b.author) {
|
|
|
|
b.authorDistance = author.length
|
|
|
|
} else {
|
2021-08-26 02:15:00 +02:00
|
|
|
b.totalPossibleDistance += b.author.length
|
2021-08-25 03:24:40 +02:00
|
|
|
b.cleanedAuthor = this.cleanAuthorForCompares(b.author)
|
|
|
|
|
|
|
|
var cleanedAuthorDistance = levenshteinDistance(b.cleanedAuthor, searchAuthor)
|
|
|
|
var authorDistance = levenshteinDistance(b.author || '', author)
|
2021-08-26 02:15:00 +02:00
|
|
|
|
2021-08-25 03:24:40 +02:00
|
|
|
// Use best distance
|
2021-08-26 02:15:00 +02:00
|
|
|
b.authorDistance = Math.min(cleanedAuthorDistance, authorDistance)
|
|
|
|
|
|
|
|
// Check book author contains searchAuthor
|
|
|
|
if (searchAuthor.length > 4 && b.cleanedAuthor.includes(searchAuthor)) b.includesAuthor = searchAuthor
|
|
|
|
else if (author.length > 4 && b.author.includes(author)) b.includesAuthor = author
|
2021-08-25 03:24:40 +02:00
|
|
|
}
|
2021-08-21 16:15:44 +02:00
|
|
|
}
|
|
|
|
b.totalDistance = b.titleDistance + (b.authorDistance || 0)
|
|
|
|
|
2021-08-26 02:15:00 +02:00
|
|
|
// Check book title contains the searchTitle
|
|
|
|
if (searchTitle.length > 4 && b.cleanedTitle.includes(searchTitle)) b.includesTitle = searchTitle
|
|
|
|
else if (title.length > 4 && b.title.includes(title)) b.includesTitle = title
|
2021-08-21 16:15:44 +02:00
|
|
|
|
|
|
|
return b
|
|
|
|
}).filter(b => {
|
2021-08-26 02:15:00 +02:00
|
|
|
if (b.includesTitle) { // If search title was found in result title then skip over leven distance check
|
2021-10-06 04:10:49 +02:00
|
|
|
if (this.verbose) Logger.debug(`Exact title was included in "${b.title}", Search: "${b.includesTitle}"`)
|
2021-08-21 16:15:44 +02:00
|
|
|
} else if (b.titleDistance > maxTitleDistance) {
|
2021-10-06 04:10:49 +02:00
|
|
|
if (this.verbose) Logger.debug(`Filtering out search result title distance = ${b.titleDistance}: "${b.cleanedTitle}"/"${searchTitle}"`)
|
2021-08-21 16:15:44 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-08-26 02:15:00 +02:00
|
|
|
if (author) {
|
|
|
|
if (b.includesAuthor) { // If search author was found in result author then skip over leven distance check
|
2021-10-06 04:10:49 +02:00
|
|
|
if (this.verbose) Logger.debug(`Exact author was included in "${b.author}", Search: "${b.includesAuthor}"`)
|
2021-08-26 02:15:00 +02:00
|
|
|
} else if (b.authorDistance > maxAuthorDistance) {
|
2021-10-06 04:10:49 +02:00
|
|
|
if (this.verbose) Logger.debug(`Filtering out search result "${b.author}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`)
|
2021-08-26 02:15:00 +02:00
|
|
|
return false
|
|
|
|
}
|
2021-08-21 16:15:44 +02:00
|
|
|
}
|
|
|
|
|
2021-08-26 02:15:00 +02:00
|
|
|
// If book total search length < 5 and was not exact match, then filter out
|
|
|
|
if (b.totalPossibleDistance < 5 && b.totalDistance > 0) return false
|
2021-08-21 16:15:44 +02:00
|
|
|
return true
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
|
|
|
var books = await this.openLibrary.searchTitle(title)
|
2021-10-06 04:10:49 +02:00
|
|
|
if (this.verbose) Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`)
|
2021-08-18 00:01:11 +02:00
|
|
|
if (books.errorCode) {
|
2021-08-21 16:15:44 +02:00
|
|
|
Logger.error(`OpenLib Search Error ${books.errorCode}`)
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
var booksFiltered = this.filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance)
|
|
|
|
if (!booksFiltered.length && books.length) {
|
2021-10-06 04:10:49 +02:00
|
|
|
if (this.verbose) Logger.debug(`Search has ${books.length} matches, but no close title matches`)
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
2023-09-30 20:08:03 +02:00
|
|
|
booksFiltered.sort((a, b) => {
|
|
|
|
return a.totalDistance - b.totalDistance
|
|
|
|
})
|
|
|
|
|
2021-08-21 16:15:44 +02:00
|
|
|
return booksFiltered
|
|
|
|
}
|
|
|
|
|
2021-12-08 01:42:56 +01:00
|
|
|
async getGoogleBooksResults(title, author) {
|
2021-10-28 21:41:42 +02:00
|
|
|
var books = await this.googleBooks.search(title, author)
|
|
|
|
if (this.verbose) Logger.debug(`GoogleBooks Book Search Results: ${books.length || 0}`)
|
|
|
|
if (books.errorCode) {
|
|
|
|
Logger.error(`GoogleBooks Search Error ${books.errorCode}`)
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
// Google has good sort
|
|
|
|
return books
|
|
|
|
}
|
|
|
|
|
2023-02-06 22:25:18 +01:00
|
|
|
async getFantLabResults(title, author) {
|
|
|
|
var books = await this.fantLab.search(title, author)
|
|
|
|
if (this.verbose) Logger.debug(`FantLab Book Search Results: ${books.length || 0}`)
|
|
|
|
if (books.errorCode) {
|
|
|
|
Logger.error(`FantLab Search Error ${books.errorCode}`)
|
|
|
|
return []
|
|
|
|
}
|
|
|
|
|
|
|
|
return books
|
|
|
|
}
|
|
|
|
|
2023-04-20 07:13:52 +02:00
|
|
|
async getAudiobookCoversResults(search) {
|
|
|
|
const covers = await this.audiobookCovers.search(search)
|
2023-04-21 23:17:52 +02:00
|
|
|
if (this.verbose) Logger.debug(`AudiobookCovers Search Results: ${covers.length || 0}`)
|
|
|
|
return covers || []
|
2023-04-20 07:13:52 +02:00
|
|
|
}
|
|
|
|
|
2022-03-07 00:26:35 +01:00
|
|
|
async getiTunesAudiobooksResults(title, author) {
|
|
|
|
return this.iTunesApi.searchAudiobooks(title)
|
|
|
|
}
|
|
|
|
|
2022-10-08 00:18:28 +02:00
|
|
|
async getAudibleResults(title, author, asin, provider) {
|
|
|
|
const region = provider.includes('.') ? provider.split('.').pop() : ''
|
|
|
|
const books = await this.audible.search(title, author, asin, region)
|
2021-11-21 19:59:32 +01:00
|
|
|
if (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
|
|
|
|
if (!books) return []
|
|
|
|
return books
|
|
|
|
}
|
|
|
|
|
2023-10-01 10:42:47 +02:00
|
|
|
static TitleCandidates = class {
|
|
|
|
|
|
|
|
constructor(bookFinder, cleanAuthor) {
|
|
|
|
this.bookFinder = bookFinder
|
|
|
|
this.candidates = new Set()
|
|
|
|
this.cleanAuthor = cleanAuthor
|
2023-10-05 12:28:55 +02:00
|
|
|
this.priorities = {}
|
|
|
|
this.positions = {}
|
2023-10-01 10:42:47 +02:00
|
|
|
}
|
|
|
|
|
2023-10-05 12:28:55 +02:00
|
|
|
add(title, position = 0) {
|
2023-10-05 13:39:29 +02:00
|
|
|
// if title contains the author, remove it
|
|
|
|
if (this.cleanAuthor) {
|
|
|
|
const authorRe = new RegExp(`(^| | by |)${this.cleanAuthor}(?= |$)`, "g")
|
|
|
|
title = this.bookFinder.cleanAuthorForCompares(title).replace(authorRe, '').trim()
|
|
|
|
}
|
|
|
|
|
2023-10-01 10:42:47 +02:00
|
|
|
const titleTransformers = [
|
|
|
|
[/([,:;_]| by ).*/g, ''], // Remove subtitle
|
|
|
|
[/(^| )\d+k(bps)?( |$)/, ' '], // Remove bitrate
|
2023-10-05 14:22:02 +02:00
|
|
|
[/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''], // Remove edition
|
|
|
|
[/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type
|
|
|
|
[/ a novel.*$/g, ''], // Remove "a novel"
|
|
|
|
[/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers
|
2023-10-01 10:42:47 +02:00
|
|
|
]
|
|
|
|
|
|
|
|
// Main variant
|
|
|
|
const cleanTitle = this.bookFinder.cleanTitleForCompares(title).trim()
|
|
|
|
if (!cleanTitle) return
|
|
|
|
this.candidates.add(cleanTitle)
|
2023-10-05 12:28:55 +02:00
|
|
|
this.priorities[cleanTitle] = 0
|
|
|
|
this.positions[cleanTitle] = position
|
2023-10-01 10:42:47 +02:00
|
|
|
|
|
|
|
let candidate = cleanTitle
|
|
|
|
|
2023-10-04 16:53:12 +02:00
|
|
|
for (const transformer of titleTransformers)
|
2023-10-01 10:42:47 +02:00
|
|
|
candidate = candidate.replace(transformer[0], transformer[1]).trim()
|
2023-10-04 16:53:12 +02:00
|
|
|
|
2023-10-05 12:28:55 +02:00
|
|
|
if (candidate != cleanTitle) {
|
|
|
|
if (candidate) {
|
|
|
|
this.candidates.add(candidate)
|
|
|
|
this.priorities[candidate] = 0
|
|
|
|
this.positions[candidate] = position
|
|
|
|
}
|
|
|
|
this.priorities[cleanTitle] = 1
|
|
|
|
}
|
2023-10-01 10:42:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
get size() {
|
|
|
|
return this.candidates.size
|
|
|
|
}
|
|
|
|
|
|
|
|
getCandidates() {
|
|
|
|
var candidates = [...this.candidates]
|
|
|
|
candidates.sort((a, b) => {
|
|
|
|
// Candidates that include the author are likely low quality
|
|
|
|
const includesAuthorDiff = !b.includes(this.cleanAuthor) - !a.includes(this.cleanAuthor)
|
|
|
|
if (includesAuthorDiff) return includesAuthorDiff
|
|
|
|
// Candidates that include only digits are also likely low quality
|
|
|
|
const onlyDigits = /^\d+$/
|
|
|
|
const includesOnlyDigitsDiff = !onlyDigits.test(b) - !onlyDigits.test(a)
|
|
|
|
if (includesOnlyDigitsDiff) return includesOnlyDigitsDiff
|
2023-10-05 12:28:55 +02:00
|
|
|
// transformed candidates receive higher priority
|
|
|
|
const priorityDiff = this.priorities[a] - this.priorities[b]
|
|
|
|
if (priorityDiff) return priorityDiff
|
|
|
|
// if same priorirty, prefer candidates that are closer to the beginning (e.g. titles before subtitles)
|
|
|
|
const positionDiff = this.positions[a] - this.positions[b]
|
|
|
|
if (positionDiff) return positionDiff
|
2023-10-01 10:42:47 +02:00
|
|
|
// Start with longer candidaets, as they are likely more specific
|
|
|
|
const lengthDiff = b.length - a.length
|
|
|
|
if (lengthDiff) return lengthDiff
|
|
|
|
return b.localeCompare(a)
|
|
|
|
})
|
|
|
|
Logger.debug(`[${this.constructor.name}] Found ${candidates.length} fuzzy title candidates`)
|
|
|
|
Logger.debug(candidates)
|
|
|
|
return candidates
|
|
|
|
}
|
|
|
|
|
|
|
|
delete(title) {
|
|
|
|
return this.candidates.delete(title)
|
|
|
|
}
|
2023-09-14 23:32:20 +02:00
|
|
|
}
|
|
|
|
|
2023-10-05 13:39:29 +02:00
|
|
|
static AuthorCandidates = class {
|
|
|
|
constructor(bookFinder, cleanAuthor) {
|
|
|
|
this.bookFinder = bookFinder
|
|
|
|
this.candidates = new Set()
|
|
|
|
this.cleanAuthor = cleanAuthor
|
|
|
|
if (cleanAuthor) this.candidates.add(cleanAuthor)
|
|
|
|
}
|
|
|
|
|
|
|
|
validateAuthor(name, region = '', maxLevenshtein = 3) {
|
|
|
|
return this.bookFinder.audnexus.authorASINsRequest(name, region).then((asins) => {
|
|
|
|
for (const asin of asins) {
|
|
|
|
let cleanName = this.bookFinder.cleanAuthorForCompares(asin.name)
|
|
|
|
if (!cleanName) continue
|
|
|
|
if (cleanName.includes(name)) return name
|
|
|
|
if (name.includes(cleanName)) return cleanName
|
|
|
|
if (levenshteinDistance(cleanName, name) <= maxLevenshtein) return cleanName
|
|
|
|
}
|
|
|
|
return ''
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
add(author) {
|
|
|
|
const authorTransformers = []
|
|
|
|
|
|
|
|
// Main variant
|
|
|
|
const cleanAuthor = this.bookFinder.cleanAuthorForCompares(author).trim()
|
|
|
|
if (!cleanAuthor) return false
|
|
|
|
this.candidates.add(cleanAuthor)
|
|
|
|
|
|
|
|
let candidate = cleanAuthor
|
|
|
|
|
|
|
|
for (const transformer of authorTransformers) {
|
|
|
|
candidate = candidate.replace(transformer[0], transformer[1]).trim()
|
|
|
|
if (candidate) {
|
|
|
|
this.candidates.add(candidate)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
get size() {
|
|
|
|
return this.candidates.size
|
|
|
|
}
|
|
|
|
|
|
|
|
async getCandidates() {
|
|
|
|
var filteredCandidates = []
|
|
|
|
var promises = []
|
|
|
|
for (const candidate of this.candidates) {
|
|
|
|
promises.push(this.validateAuthor(candidate))
|
|
|
|
}
|
|
|
|
const results = [...new Set(await Promise.all(promises))]
|
|
|
|
filteredCandidates = results.filter(author => author)
|
|
|
|
// if no valid candidates were found, add back the original clean author
|
|
|
|
if (!filteredCandidates.length && this.cleanAuthor) filteredCandidates.push(this.cleanAuthor)
|
|
|
|
// always add an empty author candidate
|
|
|
|
filteredCandidates.push('')
|
|
|
|
Logger.debug(`[${this.constructor.name}] Found ${filteredCandidates.length} fuzzy author candidates`)
|
|
|
|
Logger.debug(filteredCandidates)
|
|
|
|
return filteredCandidates
|
|
|
|
}
|
|
|
|
|
|
|
|
delete(author) {
|
|
|
|
return this.candidates.delete(author)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-01 10:42:47 +02:00
|
|
|
|
2023-09-22 23:03:41 +02:00
|
|
|
/**
|
|
|
|
* Search for books including fuzzy searches
|
|
|
|
*
|
|
|
|
* @param {string} provider
|
|
|
|
* @param {string} title
|
|
|
|
* @param {string} author
|
|
|
|
* @param {string} isbn
|
|
|
|
* @param {string} asin
|
|
|
|
* @param {{titleDistance:number, authorDistance:number, maxFuzzySearches:number}} options
|
|
|
|
* @returns {Promise<Object[]>}
|
|
|
|
*/
|
2022-05-23 04:56:51 +02:00
|
|
|
async search(provider, title, author, isbn, asin, options = {}) {
|
2023-09-22 23:03:41 +02:00
|
|
|
let books = []
|
2023-09-14 23:32:20 +02:00
|
|
|
const maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
|
|
|
|
const maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
|
2023-09-15 11:24:19 +02:00
|
|
|
const maxFuzzySearches = !isNaN(options.maxFuzzySearches) ? Number(options.maxFuzzySearches) : 5
|
2023-09-22 23:03:41 +02:00
|
|
|
let numFuzzySearches = 0
|
2023-09-14 23:32:20 +02:00
|
|
|
|
|
|
|
if (!title)
|
|
|
|
return books
|
|
|
|
|
|
|
|
books = await this.runSearch(title, author, provider, asin, maxTitleDistance, maxAuthorDistance)
|
|
|
|
|
|
|
|
if (!books.length && maxFuzzySearches > 0) {
|
|
|
|
// normalize title and author
|
|
|
|
title = title.trim().toLowerCase()
|
|
|
|
author = author.trim().toLowerCase()
|
|
|
|
|
2023-10-01 10:42:47 +02:00
|
|
|
const cleanAuthor = this.cleanAuthorForCompares(author)
|
|
|
|
|
2023-09-14 23:32:20 +02:00
|
|
|
// Now run up to maxFuzzySearches fuzzy searches
|
2023-10-05 13:39:29 +02:00
|
|
|
let authorCandidates = new BookFinder.AuthorCandidates(this, cleanAuthor)
|
2023-09-14 23:32:20 +02:00
|
|
|
|
2023-10-05 19:53:54 +02:00
|
|
|
// remove underscores and parentheses with their contents, and replace with a separator
|
|
|
|
const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, " - ")
|
2023-09-14 23:32:20 +02:00
|
|
|
// Split title into hypen-separated parts
|
|
|
|
const titleParts = cleanTitle.split(/ - | -|- /)
|
2023-10-05 13:39:29 +02:00
|
|
|
for (const titlePart of titleParts)
|
|
|
|
authorCandidates.add(titlePart)
|
|
|
|
authorCandidates = await authorCandidates.getCandidates()
|
|
|
|
for (const authorCandidate of authorCandidates) {
|
|
|
|
let titleCandidates = new BookFinder.TitleCandidates(this, authorCandidate)
|
|
|
|
for (const [position, titlePart] of titleParts.entries())
|
|
|
|
titleCandidates.add(titlePart, position)
|
2023-10-01 10:42:47 +02:00
|
|
|
titleCandidates = titleCandidates.getCandidates()
|
|
|
|
for (const titleCandidate of titleCandidates) {
|
2023-10-05 13:39:29 +02:00
|
|
|
if (titleCandidate == title && authorCandidate == author) continue // We already tried this
|
2023-09-14 23:32:20 +02:00
|
|
|
if (++numFuzzySearches > maxFuzzySearches) return books
|
2023-10-05 13:39:29 +02:00
|
|
|
books = await this.runSearch(titleCandidate, authorCandidate, provider, asin, maxTitleDistance, maxAuthorDistance)
|
|
|
|
if (books.length) return books
|
2023-09-14 23:32:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return books
|
|
|
|
}
|
|
|
|
|
2023-09-22 23:03:41 +02:00
|
|
|
/**
|
|
|
|
* Search for books
|
|
|
|
*
|
|
|
|
* @param {string} title
|
|
|
|
* @param {string} author
|
|
|
|
* @param {string} provider
|
|
|
|
* @param {string} asin only used for audible providers
|
|
|
|
* @param {number} maxTitleDistance only used for openlibrary provider
|
|
|
|
* @param {number} maxAuthorDistance only used for openlibrary provider
|
|
|
|
* @returns {Promise<Object[]>}
|
|
|
|
*/
|
2023-09-14 23:32:20 +02:00
|
|
|
async runSearch(title, author, provider, asin, maxTitleDistance, maxAuthorDistance) {
|
2023-05-14 20:43:20 +02:00
|
|
|
Logger.debug(`Book Search: title: "${title}", author: "${author || ''}", provider: ${provider}`)
|
2021-08-21 16:15:44 +02:00
|
|
|
|
2023-09-22 23:03:41 +02:00
|
|
|
let books = []
|
2023-09-14 23:32:20 +02:00
|
|
|
|
2021-10-28 21:41:42 +02:00
|
|
|
if (provider === 'google') {
|
2022-06-30 04:25:59 +02:00
|
|
|
books = await this.getGoogleBooksResults(title, author)
|
2022-10-08 00:18:28 +02:00
|
|
|
} else if (provider.startsWith('audible')) {
|
|
|
|
books = await this.getAudibleResults(title, author, asin, provider)
|
2022-03-07 00:26:35 +01:00
|
|
|
} else if (provider === 'itunes') {
|
2022-06-30 04:19:58 +02:00
|
|
|
books = await this.getiTunesAudiobooksResults(title, author)
|
2021-08-21 16:15:44 +02:00
|
|
|
} else if (provider === 'openlibrary') {
|
|
|
|
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
2023-02-06 22:25:18 +01:00
|
|
|
} else if (provider === 'fantlab') {
|
|
|
|
books = await this.getFantLabResults(title, author)
|
2023-04-20 07:13:52 +02:00
|
|
|
} else if (provider === 'audiobookcovers') {
|
|
|
|
books = await this.getAudiobookCoversResults(title)
|
2023-02-06 22:25:18 +01:00
|
|
|
}
|
|
|
|
else {
|
2022-07-05 02:14:52 +02:00
|
|
|
books = await this.getGoogleBooksResults(title, author)
|
2021-08-21 16:15:44 +02:00
|
|
|
}
|
2023-04-21 23:17:52 +02:00
|
|
|
return books
|
2021-08-21 16:15:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async findCovers(provider, title, author, options = {}) {
|
2023-05-14 20:43:20 +02:00
|
|
|
let searchResults = []
|
|
|
|
|
|
|
|
if (provider === 'all') {
|
|
|
|
for (const providerString of this.providers) {
|
|
|
|
const providerResults = await this.search(providerString, title, author, options)
|
|
|
|
Logger.debug(`[BookFinder] Found ${providerResults.length} covers from ${providerString}`)
|
|
|
|
searchResults.push(...providerResults)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
searchResults = await this.search(provider, title, author, options)
|
|
|
|
}
|
2021-08-26 02:15:00 +02:00
|
|
|
Logger.debug(`[BookFinder] FindCovers search results: ${searchResults.length}`)
|
2021-08-25 03:24:40 +02:00
|
|
|
|
2023-05-14 20:43:20 +02:00
|
|
|
const covers = []
|
2021-08-21 16:15:44 +02:00
|
|
|
searchResults.forEach((result) => {
|
|
|
|
if (result.covers && result.covers.length) {
|
2023-05-14 20:43:20 +02:00
|
|
|
covers.push(...result.covers)
|
2021-08-21 16:15:44 +02:00
|
|
|
}
|
|
|
|
if (result.cover) {
|
|
|
|
covers.push(result.cover)
|
|
|
|
}
|
|
|
|
})
|
2023-05-14 20:43:20 +02:00
|
|
|
return [...(new Set(covers))]
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
2022-05-11 00:03:41 +02:00
|
|
|
|
2022-10-15 22:31:07 +02:00
|
|
|
findChapters(asin, region) {
|
|
|
|
return this.audnexus.getChaptersByASIN(asin, region)
|
2022-05-11 00:03:41 +02:00
|
|
|
}
|
2021-08-18 00:01:11 +02:00
|
|
|
}
|
2023-09-14 23:32:20 +02:00
|
|
|
module.exports = new BookFinder()
|