diff --git a/client/components/modals/libraries/LibraryItem.vue b/client/components/modals/libraries/LibraryItem.vue
index 37c1d692..6f6b5e8a 100644
--- a/client/components/modals/libraries/LibraryItem.vue
+++ b/client/components/modals/libraries/LibraryItem.vue
@@ -9,8 +9,11 @@
@@ -59,6 +62,18 @@ export default {
}
},
methods: {
+ matchAll() {
+ this.$axios
+ .$post(`/api/libraries/${this.library.id}/matchbooks`)
+ .then(() => {
+ console.log('Starting scan for matches')
+ })
+ .catch((error) => {
+ console.error('Failed', error)
+ var errorMsg = err.response ? err.response.data : ''
+ this.$toast.error(errorMsg || 'Match all failed')
+ })
+ },
editClick() {
this.$emit('edit', this.library)
},
diff --git a/client/components/tables/LibrariesTable.vue b/client/components/tables/LibrariesTable.vue
index 4500d08c..eaa2b01c 100644
--- a/client/components/tables/LibrariesTable.vue
+++ b/client/components/tables/LibrariesTable.vue
@@ -16,6 +16,8 @@
*Force Re-Scan will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be probed/parsed and used for book details.
+
+
**Match Books will attempt to match books in library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.
diff --git a/client/layouts/default.vue b/client/layouts/default.vue
index c40f8eae..6012bf2d 100644
--- a/client/layouts/default.vue
+++ b/client/layouts/default.vue
@@ -205,7 +205,7 @@ export default {
scanComplete(data) {
console.log('Scan complete received', data)
- var message = `Scan "${data.name}" complete!`
+ var message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" complete!`
if (data.results) {
var scanResultMsgs = []
var results = data.results
@@ -216,7 +216,7 @@ export default {
if (!scanResultMsgs.length) message += '\nEverything was up to date'
else message += '\n' + scanResultMsgs.join('\n')
} else {
- message = `Scan "${data.name}" was canceled`
+ message = `${data.type === 'match' ? 'Match' : 'Scan'} "${data.name}" was canceled`
}
var existingScan = this.$store.getters['scanners/getLibraryScan'](data.id)
@@ -232,7 +232,7 @@ export default {
this.$root.socket.emit('cancel_scan', id)
},
scanStart(data) {
- data.toastId = this.$toast(`Scanning "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, position: 'bottom-center', onClose: () => this.onScanToastCancel(data.id) })
+ data.toastId = this.$toast(`${data.type === 'match' ? 'Matching' : 'Scanning'} "${data.name}"...`, { timeout: false, type: 'info', draggable: false, closeOnClick: false, closeButton: CloseButton, closeButtonClassName: 'cancel-scan-btn', showCloseButtonOnHover: false, position: 'bottom-center', onClose: () => this.onScanToastCancel(data.id) })
this.$store.commit('scanners/addUpdate', data)
},
scanProgress(data) {
diff --git a/server/ApiController.js b/server/ApiController.js
index db166945..edb0b661 100644
--- a/server/ApiController.js
+++ b/server/ApiController.js
@@ -18,9 +18,10 @@ const AuthorFinder = require('./AuthorFinder')
const FileSystemController = require('./controllers/FileSystemController')
class ApiController {
- constructor(MetadataPath, db, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) {
+ constructor(MetadataPath, db, auth, scanner, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) {
this.db = db
this.auth = auth
+ this.scanner = scanner
this.streamManager = streamManager
this.rssFeeds = rssFeeds
this.downloadManager = downloadManager
@@ -517,57 +518,5 @@ class ApiController {
await this.cacheManager.purgeAll()
res.sendStatus(200)
}
-
- async quickMatchBook(audiobook, options = {}) {
- var provider = options.provider || 'google'
- var searchTitle = options.title || audiobook.book._title
- var searchAuthor = options.author || audiobook.book._author
-
- var results = await this.bookFinder.search(provider, searchTitle, searchAuthor)
- if (!results.length) {
- return {
- warning: `No ${provider} match found`
- }
- }
- var matchData = results[0]
-
- // Update cover if not set OR overrideCover flag
- var hasUpdated = false
- if (matchData.cover && (!audiobook.book.cover || options.overrideCover)) {
- Logger.debug(`[BookController] Updating cover "${matchData.cover}"`)
- var coverResult = await this.coverController.downloadCoverFromUrl(audiobook, matchData.cover)
- if (!coverResult || coverResult.error || !coverResult.cover) {
- Logger.warn(`[BookController] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
- } else {
- hasUpdated = true
- }
- }
-
- // Update book details if not set OR overrideDetails flag
- const detailKeysToUpdate = ['title', 'subtitle', 'author', 'narrator', 'publisher', 'publishYear', 'series', 'volumeNumber', 'asin', 'isbn']
- const updatePayload = {}
- for (const key in matchData) {
- if (matchData[key] && detailKeysToUpdate.includes(key) && (!audiobook.book[key] || options.overrideDetails)) {
- updatePayload[key] = matchData[key]
- }
- }
-
- if (Object.keys(updatePayload).length) {
- Logger.debug('[BookController] Updating details', updatePayload)
- if (audiobook.update({ book: updatePayload })) {
- hasUpdated = true
- }
- }
-
- if (hasUpdated) {
- await this.db.updateEntity('audiobook', audiobook)
- this.emitter('audiobook_updated', audiobook.toJSONExpanded())
- }
-
- return {
- updated: hasUpdated,
- audiobook: audiobook.toJSONExpanded()
- }
- }
}
module.exports = ApiController
\ No newline at end of file
diff --git a/server/Server.js b/server/Server.js
index 3c140596..1bd5da04 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -55,7 +55,7 @@ class Server {
this.streamManager = new StreamManager(this.db, this.MetadataPath, this.emitter.bind(this), this.clientEmitter.bind(this))
this.rssFeeds = new RssFeeds(this.Port, this.db)
this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.Uid, this.Gid)
- this.apiController = new ApiController(this.MetadataPath, this.db, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
+ this.apiController = new ApiController(this.MetadataPath, this.db, this.auth, this.scanner, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this))
this.hlsController = new HlsController(this.db, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath)
Logger.logManager = this.logManager
diff --git a/server/controllers/BookController.js b/server/controllers/BookController.js
index d6e579db..5a50f638 100644
--- a/server/controllers/BookController.js
+++ b/server/controllers/BookController.js
@@ -272,7 +272,7 @@ class BookController {
}
var options = req.body || {}
- var matchResult = await this.quickMatchBook(audiobook, options)
+ var matchResult = await this.scanner.quickMatchBook(audiobook, options)
res.json(matchResult)
}
}
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 93afe740..dc699390 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -475,28 +475,8 @@ class LibraryController {
Logger.error(`[LibraryController] Non-root user attempted to match library books`, req.user)
return res.sendStatus(403)
}
+ this.scanner.matchLibraryBooks(req.library)
res.sendStatus(200)
-
- const provider = req.library.provider || 'google'
- var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
- const resultPayload = {
- library: library.toJSON(),
- total: audiobooksInLibrary.length,
- updated: 0
- }
-
- for (let i = 0; i < audiobooksInLibrary.length; i++) {
- var audiobook = audiobooksInLibrary[i]
- Logger.debug(`[LibraryController] matchBooks quick matching "${audiobook.title}" (${i + 1} of ${audiobooksInLibrary.length})`)
- var result = await this.quickMatchBook(audiobook, { provider })
- if (result.warning) {
- Logger.warn(`[LibraryController] matchBooks warning ${result.warning} for audiobook "${audiobook.title}"`)
- } else if (result.updated) {
- resultPayload.updated++
- }
- }
-
- this.clientEmitter(req.usr.id, 'library-match-results', resultPayload)
}
middleware(req, res, next) {
diff --git a/server/providers/Audible.js b/server/providers/Audible.js
index 2f369937..c50da3f4 100644
--- a/server/providers/Audible.js
+++ b/server/providers/Audible.js
@@ -26,8 +26,10 @@ class Audible {
}
getBestImageLink(images) {
- var keys = Object.keys(images);
- return images[keys[keys.length - 1]];
+ if (!images) return null
+ var keys = Object.keys(images)
+ if (!keys.length) return null
+ return images[keys[keys.length - 1]]
}
getPrimarySeries(series, publication_name) {
diff --git a/server/scanner/LibraryScan.js b/server/scanner/LibraryScan.js
index a7a0809d..d21574e1 100644
--- a/server/scanner/LibraryScan.js
+++ b/server/scanner/LibraryScan.js
@@ -10,6 +10,7 @@ const { getId, secondsToTimestamp } = require('../utils/index')
class LibraryScan {
constructor() {
this.id = null
+ this.type = null
this.libraryId = null
this.libraryName = null
this.folders = null
@@ -46,6 +47,7 @@ class LibraryScan {
get getScanEmitData() {
return {
id: this.libraryId,
+ type: this.type,
name: this.libraryName,
results: {
added: this.resultsAdded,
@@ -64,10 +66,11 @@ class LibraryScan {
toJSON() {
return {
id: this.id,
+ type: this.type,
libraryId: this.libraryId,
libraryName: this.libraryName,
folders: this.folders.map(f => f.toJSON()),
- scanOptions: this.scanOptions.toJSON(),
+ scanOptions: this.scanOptions ? this.scanOptions.toJSON() : null,
startedAt: this.startedAt,
finishedAt: this.finishedAt,
elapsed: this.elapsed,
@@ -77,8 +80,9 @@ class LibraryScan {
}
}
- setData(library, scanOptions) {
+ setData(library, scanOptions, type = 'scan') {
this.id = getId('lscan')
+ this.type = type
this.libraryId = library.id
this.libraryName = library.name
this.folders = library.folders.map(folder => new Folder(folder.toJSON()))
diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js
index cf6aea17..627def2c 100644
--- a/server/scanner/Scanner.js
+++ b/server/scanner/Scanner.js
@@ -625,5 +625,101 @@ class Scanner {
Logger.info(`[Scanner] Updated ${audiobooksUpdated} audiobook IDs`)
}
}
+
+ async quickMatchBook(audiobook, options = {}) {
+ var provider = options.provider || 'google'
+ var searchTitle = options.title || audiobook.book._title
+ var searchAuthor = options.author || audiobook.book._author
+
+ var results = await this.bookFinder.search(provider, searchTitle, searchAuthor)
+ if (!results.length) {
+ return {
+ warning: `No ${provider} match found`
+ }
+ }
+ var matchData = results[0]
+
+ // Update cover if not set OR overrideCover flag
+ var hasUpdated = false
+ if (matchData.cover && (!audiobook.book.cover || options.overrideCover)) {
+ Logger.debug(`[BookController] Updating cover "${matchData.cover}"`)
+ var coverResult = await this.coverController.downloadCoverFromUrl(audiobook, matchData.cover)
+ if (!coverResult || coverResult.error || !coverResult.cover) {
+ Logger.warn(`[BookController] Match cover "${matchData.cover}" failed to use: ${coverResult ? coverResult.error : 'Unknown Error'}`)
+ } else {
+ hasUpdated = true
+ }
+ }
+
+ // Update book details if not set OR overrideDetails flag
+ const detailKeysToUpdate = ['title', 'subtitle', 'author', 'narrator', 'publisher', 'publishYear', 'series', 'volumeNumber', 'asin', 'isbn']
+ const updatePayload = {}
+ for (const key in matchData) {
+ if (matchData[key] && detailKeysToUpdate.includes(key) && (!audiobook.book[key] || options.overrideDetails)) {
+ updatePayload[key] = matchData[key]
+ }
+ }
+
+ if (Object.keys(updatePayload).length) {
+ Logger.debug('[BookController] Updating details', updatePayload)
+ if (audiobook.update({ book: updatePayload })) {
+ hasUpdated = true
+ }
+ }
+
+ if (hasUpdated) {
+ await this.db.updateEntity('audiobook', audiobook)
+ this.emitter('audiobook_updated', audiobook.toJSONExpanded())
+ }
+
+ return {
+ updated: hasUpdated,
+ audiobook: audiobook.toJSONExpanded()
+ }
+ }
+
+ async matchLibraryBooks(library) {
+ if (this.isLibraryScanning(library.id)) {
+ Logger.error(`[Scanner] Already scanning ${library.id}`)
+ return
+ }
+
+ const provider = library.provider || 'google'
+ var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
+ if (!audiobooksInLibrary.length) {
+ return
+ }
+
+ var libraryScan = new LibraryScan()
+ libraryScan.setData(library, null, 'match')
+ this.librariesScanning.push(libraryScan.getScanEmitData)
+ this.emitter('scan_start', libraryScan.getScanEmitData)
+
+ Logger.info(`[Scanner] Starting library match books scan ${libraryScan.id} for ${libraryScan.libraryName}`)
+
+ for (let i = 0; i < audiobooksInLibrary.length; i++) {
+ var audiobook = audiobooksInLibrary[i]
+ Logger.debug(`[Scanner] Quick matching "${audiobook.title}" (${i + 1} of ${audiobooksInLibrary.length})`)
+ var result = await this.quickMatchBook(audiobook, { provider })
+ if (result.warning) {
+ Logger.warn(`[Scanner] Match warning ${result.warning} for audiobook "${audiobook.title}"`)
+ } else if (result.updated) {
+ libraryScan.resultsUpdated++
+ }
+
+ if (this.cancelLibraryScan[libraryScan.libraryId]) {
+ Logger.info(`[Scanner] Library match scan canceled for "${libraryScan.libraryName}"`)
+ delete this.cancelLibraryScan[libraryScan.libraryId]
+ var scanData = libraryScan.getScanEmitData
+ scanData.results = false
+ this.emitter('scan_complete', scanData)
+ this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
+ return
+ }
+ }
+
+ this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
+ this.emitter('scan_complete', libraryScan.getScanEmitData)
+ }
}
module.exports = Scanner
\ No newline at end of file