-
+
{{ label }}{{ note }}
diff --git a/client/components/ui/TextareaInput.vue b/client/components/ui/TextareaInput.vue
index ab471860..5f5080a9 100644
--- a/client/components/ui/TextareaInput.vue
+++ b/client/components/ui/TextareaInput.vue
@@ -38,10 +38,11 @@ export default {
\ No newline at end of file
diff --git a/client/components/ui/TextareaWithLabel.vue b/client/components/ui/TextareaWithLabel.vue
index 747ccc3d..aa28a716 100644
--- a/client/components/ui/TextareaWithLabel.vue
+++ b/client/components/ui/TextareaWithLabel.vue
@@ -1,7 +1,7 @@
-
{{ label }}
-
+
{{ label }}
+
@@ -10,6 +10,7 @@ export default {
props: {
value: [String, Number],
label: String,
+ disabled: Boolean,
rows: {
type: Number,
default: 2
diff --git a/client/package.json b/client/package.json
index 232c64b4..47c0d8bb 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
- "version": "1.5.5",
+ "version": "1.5.6",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {
diff --git a/package.json b/package.json
index fef12838..8f0e7f2d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "1.5.5",
+ "version": "1.5.6",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {
diff --git a/server/ApiController.js b/server/ApiController.js
index a16f21aa..7e95a8e1 100644
--- a/server/ApiController.js
+++ b/server/ApiController.js
@@ -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)
diff --git a/server/BookFinder.js b/server/BookFinder.js
index fb7b45f4..acf00b20 100644
--- a/server/BookFinder.js
+++ b/server/BookFinder.js
@@ -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)
diff --git a/server/Scanner.js b/server/Scanner.js
index 2322e252..130db58c 100644
--- a/server/Scanner.js
+++ b/server/Scanner.js
@@ -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')
diff --git a/server/objects/Audiobook.js b/server/objects/Audiobook.js
index 36ffe365..be4528c2 100644
--- a/server/objects/Audiobook.js
+++ b/server/objects/Audiobook.js
@@ -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 || [] : []
}
diff --git a/server/objects/Book.js b/server/objects/Book.js
index 30539b10..c8583905 100644
--- a/server/objects/Book.js
+++ b/server/objects/Book.js
@@ -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 || []
diff --git a/server/providers/GoogleBooks.js b/server/providers/GoogleBooks.js
new file mode 100644
index 00000000..43bdf719
--- /dev/null
+++ b/server/providers/GoogleBooks.js
@@ -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
\ No newline at end of file
diff --git a/server/providers/OpenLibrary.js b/server/providers/OpenLibrary.js
index 284d9477..6694005e 100644
--- a/server/providers/OpenLibrary.js
+++ b/server/providers/OpenLibrary.js
@@ -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