mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-04 03:17:00 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			158 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			158 lines
		
	
	
		
			6.0 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const OpenLibrary = require('./providers/OpenLibrary')
 | 
						|
const LibGen = require('./providers/LibGen')
 | 
						|
const Logger = require('./Logger')
 | 
						|
const { levenshteinDistance } = require('./utils/index')
 | 
						|
 | 
						|
class BookFinder {
 | 
						|
  constructor() {
 | 
						|
    this.openLibrary = new OpenLibrary()
 | 
						|
    this.libGen = new LibGen()
 | 
						|
  }
 | 
						|
 | 
						|
  async findByISBN(isbn) {
 | 
						|
    var book = await this.openLibrary.isbnLookup(isbn)
 | 
						|
    if (book.errorCode) {
 | 
						|
      console.error('Book not found')
 | 
						|
    }
 | 
						|
    return book
 | 
						|
  }
 | 
						|
 | 
						|
  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.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
 | 
						|
    } else if (provider === 'openlibrary') {
 | 
						|
      books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
 | 
						|
    } else if (provider === 'all') {
 | 
						|
      var lbBooks = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
 | 
						|
      var olBooks = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
 | 
						|
      books = books.concat(lbBooks, olBooks)
 | 
						|
    } else {
 | 
						|
      var olBooks = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
 | 
						|
      var hasCloseMatch = olBooks.find(b => (b.totalDistance < 4 && b.totalPossibleDistance > 4))
 | 
						|
      if (hasCloseMatch) {
 | 
						|
        books = olBooks
 | 
						|
      } else {
 | 
						|
        Logger.info(`Book Search, LibGen has no close matches - get openlib results also`)
 | 
						|
        var lbBooks = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
 | 
						|
        books = books.concat(lbBooks)
 | 
						|
      }
 | 
						|
 | 
						|
      if (!books.length && author) {
 | 
						|
        Logger.info(`Book Search, no matches for title and author.. check title only`)
 | 
						|
        return this.search(provider, title, null, options)
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    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 |