Add: experimental match tab with google books search #59, Add: isbn field for books #59

This commit is contained in:
advplyr 2021-10-28 14:41:42 -05:00
parent 7c1789a7c2
commit ad4dad1c29
18 changed files with 311 additions and 55 deletions

View file

@ -6,6 +6,8 @@ const Logger = require('./Logger')
const { isObject } = require('./utils/index')
const audioFileScanner = require('./utils/audioFileScanner')
const BookFinder = require('./BookFinder')
const Library = require('./objects/Library')
const User = require('./objects/User')
@ -24,6 +26,8 @@ class ApiController {
this.clientEmitter = clientEmitter
this.MetadataPath = MetadataPath
this.bookFinder = new BookFinder()
this.router = express()
this.init()
}
@ -51,6 +55,7 @@ class ApiController {
this.router.patch('/audiobook/:id/tracks', this.updateAudiobookTracks.bind(this))
this.router.post('/audiobook/:id/cover', this.uploadAudiobookCover.bind(this))
this.router.patch('/audiobook/:id/coverfile', this.updateAudiobookCoverFromFile.bind(this))
this.router.get('/audiobook/:id/match', this.matchAudiobookBook.bind(this))
this.router.patch('/audiobook/:id', this.updateAudiobook.bind(this))
this.router.patch('/match/:id', this.match.bind(this))
@ -85,8 +90,12 @@ class ApiController {
this.router.get('/scantracks/:id', this.scanAudioTrackNums.bind(this))
}
find(req, res) {
this.scanner.find(req, res)
async find(req, res) {
var provider = req.query.provider || 'google'
var title = req.query.title || ''
var author = req.query.author || ''
var results = await this.bookFinder.search(provider, title, author)
res.json(results)
}
findCovers(req, res) {
@ -497,6 +506,18 @@ class ApiController {
else res.status(200).send('No update was made to cover')
}
async matchAudiobookBook(req, res) {
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
var provider = req.query.provider || 'google'
var excludeAuthor = req.query.excludeAuthor === '1'
var authorSearch = excludeAuthor ? null : audiobook.authorFL
var results = await this.bookFinder.search(provider, audiobook.title, authorSearch)
res.json(results)
}
async updateAudiobook(req, res) {
if (!req.user.canUpdate) {
Logger.warn('User attempted to update without permission', req.user)

View file

@ -1,5 +1,6 @@
const OpenLibrary = require('./providers/OpenLibrary')
const LibGen = require('./providers/LibGen')
const GoogleBooks = require('./providers/GoogleBooks')
const Logger = require('./Logger')
const { levenshteinDistance } = require('./utils/index')
@ -7,6 +8,7 @@ class BookFinder {
constructor() {
this.openLibrary = new OpenLibrary()
this.libGen = new LibGen()
this.googleBooks = new GoogleBooks()
this.verbose = false
}
@ -143,13 +145,26 @@ class BookFinder {
return booksFiltered
}
async getGoogleBooksResults(title, author, maxTitleDistance, maxAuthorDistance) {
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
}
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.debug(`Cover Search: title: "${title}", author: "${author}", provider: ${provider}`)
if (provider === 'libgen') {
if (provider === 'google') {
return this.getGoogleBooksResults(title, author, maxTitleDistance, maxAuthorDistance)
} else if (provider === 'libgen') {
books = await this.getLibGenResults(title, author, maxTitleDistance, maxAuthorDistance)
} else if (provider === 'openlibrary') {
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)

View file

@ -10,7 +10,6 @@ const { comparePaths, getIno } = require('./utils/index')
const { secondsToTimestamp } = require('./utils/fileUtils')
const { ScanResult, CoverDestination } = require('./utils/constants')
// Classes
const BookFinder = require('./BookFinder')
const Audiobook = require('./objects/Audiobook')

View file

@ -91,6 +91,10 @@ class Audiobook {
return this.book ? this.book.authorLF : null
}
get authorFL() {
return this.book ? this.book.authorFL : null
}
get genres() {
return this.book ? this.book.genres || [] : []
}

View file

@ -1,4 +1,3 @@
const fs = require('fs-extra')
const Path = require('path')
const Logger = require('../Logger')
const parseAuthors = require('../utils/parseAuthors')
@ -16,6 +15,7 @@ class Book {
this.publishYear = null
this.publisher = null
this.description = null
this.isbn = null
this.cover = null
this.coverFullPath = null
this.genres = []
@ -56,6 +56,7 @@ class Book {
this.publishYear = book.publishYear
this.publisher = book.publisher
this.description = book.description
this.isbn = book.isbn || null
this.cover = book.cover
this.coverFullPath = book.coverFullPath || null
this.genres = book.genres
@ -78,6 +79,7 @@ class Book {
publishYear: this.publishYear,
publisher: this.publisher,
description: this.description,
isbn: this.isbn,
cover: this.cover,
coverFullPath: this.coverFullPath,
genres: this.genres,
@ -116,6 +118,7 @@ class Book {
this.volumeNumber = data.volumeNumber || null
this.publishYear = data.publishYear || null
this.description = data.description || null
this.isbn = data.isbn || null
this.cover = data.cover || null
this.coverFullPath = data.coverFullPath || null
this.genres = data.genres || []

View file

@ -0,0 +1,50 @@
const axios = require('axios')
const Logger = require('../Logger')
class GoogleBooks {
constructor() { }
extractIsbn(industryIdentifiers) {
if (!industryIdentifiers || !industryIdentifiers.length) return null
var isbnObj = industryIdentifiers.find(i => i.type === 'ISBN_13') || industryIdentifiers.find(i => i.type === 'ISBN_10')
if (isbnObj && isbnObj.identifier) return isbnObj.identifier
return null
}
cleanResult(item) {
var { id, volumeInfo } = item
if (!volumeInfo) return null
var { title, subtitle, authors, publisher, publisherDate, description, industryIdentifiers, categories, imageLinks } = volumeInfo
return {
id,
title,
subtitle: subtitle || null,
author: authors ? authors.join(', ') : null,
publisher,
publishYear: publisherDate ? publisherDate.split('-')[0] : null,
description,
cover: imageLinks && imageLinks.thumbnail ? imageLinks.thumbnail : null,
genres: categories ? categories.join(', ') : null,
isbn: this.extractIsbn(industryIdentifiers)
}
}
async search(title, author) {
var queryString = `q=intitle:${title}`
if (author) queryString += `+inauthor:${author}`
var url = `https://www.googleapis.com/books/v1/volumes?${queryString}`
Logger.debug(`[GoogleBooks] Search url: ${url}`)
var items = await axios.get(url).then((res) => {
if (!res || !res.data || !res.data.items) return []
return res.data.items
}).catch(error => {
Logger.error('[GoogleBooks] Volume search error', error)
return []
})
return items.map(item => this.cleanResult(item))
}
}
module.exports = GoogleBooks

View file

@ -51,12 +51,21 @@ class OpenLibrary {
}
}
parsePublishYear(doc, worksData) {
if (doc.first_publish_year && !isNaN(doc.first_publish_year)) return doc.first_publish_year
if (worksData.first_publish_date) {
var year = worksData.first_publish_date.split('-')[0]
if (!isNaN(year)) return year
}
return null
}
async cleanSearchDoc(doc) {
var worksData = await this.getWorksData(doc.key)
return {
title: doc.title,
author: doc.author_name ? doc.author_name.join(', ') : null,
year: doc.first_publish_year,
publishYear: this.parsePublishYear(doc, worksData),
edition: doc.cover_edition_key,
cover: doc.cover_edition_key ? `https://covers.openlibrary.org/b/OLID/${doc.cover_edition_key}-L.jpg` : null,
...worksData