diff --git a/client/components/modals/edit-tabs/Details.vue b/client/components/modals/edit-tabs/Details.vue
index 919841dc..682971f3 100644
--- a/client/components/modals/edit-tabs/Details.vue
+++ b/client/components/modals/edit-tabs/Details.vue
@@ -72,6 +72,10 @@
Save Metadata
+
+ Quick Match
+
+
Re-Scan
@@ -113,7 +117,8 @@ export default {
resettingProgress: false,
isScrollable: false,
savingMetadata: false,
- rescanning: false
+ rescanning: false,
+ quickMatching: false
}
},
watch: {
@@ -163,12 +168,41 @@ export default {
libraryId() {
return this.audiobook ? this.audiobook.libraryId : null
},
+ libraryProvider() {
+ return this.$store.getters['libraries/getLibraryProvider'](this.libraryId) || 'google'
+ },
libraryScan() {
if (!this.libraryId) return null
return this.$store.getters['scanners/getLibraryScan'](this.libraryId)
}
},
methods: {
+ quickMatch() {
+ this.quickMatching = true
+ var matchOptions = {
+ provider: this.libraryProvider,
+ title: details.title,
+ author: details.author !== this.book.author ? details.author : null
+ }
+ this.$axios
+ .$post(`/api/books/${this.audiobookId}/match`, matchOptions)
+ .then((res) => {
+ this.quickMatching = false
+ if (res.warning) {
+ this.$toast.warning(res.warning)
+ } else if (res.updated) {
+ this.$toast.success('Audiobook details updated')
+ } else {
+ this.$toast.info('No updates were made')
+ }
+ })
+ .catch((error) => {
+ var errMsg = error.response ? error.response.data || '' : ''
+ console.error('Failed to match', error)
+ this.$toast.error(errMsg || 'Failed to match')
+ this.quickMatching = false
+ })
+ },
audiobookScanComplete(result) {
this.rescanning = false
if (!result) {
diff --git a/client/components/modals/libraries/EditLibrary.vue b/client/components/modals/libraries/EditLibrary.vue
index 5cb8e794..ed7db18d 100644
--- a/client/components/modals/libraries/EditLibrary.vue
+++ b/client/components/modals/libraries/EditLibrary.vue
@@ -69,7 +69,7 @@ export default {
var newfolderpaths = this.folderPaths.join(',')
var origfolderpaths = this.library.folders.map((f) => f.fullPath).join(',')
- return newfolderpaths === origfolderpaths && this.name === this.library.name
+ return newfolderpaths === origfolderpaths && this.name === this.library.name && this.provider === this.library.provider
},
providers() {
return this.$store.state.scanners.providers
diff --git a/client/store/libraries.js b/client/store/libraries.js
index 29b776cd..696a3799 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -20,6 +20,11 @@ export const getters = {
},
getSortedLibraries: state => () => {
return state.libraries.map(lib => ({ ...lib })).sort((a, b) => a.displayOrder - b.displayOrder)
+ },
+ getLibraryProvider: state => libraryId => {
+ var library = state.libraries.find(l => l.id === libraryId)
+ if (!library) return null
+ return library.provider
}
}
diff --git a/server/ApiController.js b/server/ApiController.js
index 6a13130e..bf724f91 100644
--- a/server/ApiController.js
+++ b/server/ApiController.js
@@ -82,6 +82,7 @@ class ApiController {
this.router.post('/books/:id/cover', BookController.uploadCover.bind(this))
this.router.get('/books/:id/cover', BookController.getCover.bind(this))
this.router.patch('/books/:id/coverfile', BookController.updateCoverFromFile.bind(this))
+ this.router.post('/books/:id/match', BookController.match.bind(this))
// TEMP: Support old syntax for mobile app
this.router.get('/audiobooks', BookController.findAll.bind(this)) // Old route should pass library id
diff --git a/server/controllers/BookController.js b/server/controllers/BookController.js
index 4e6313d9..49bad422 100644
--- a/server/controllers/BookController.js
+++ b/server/controllers/BookController.js
@@ -18,9 +18,6 @@ class BookController {
}
findOne(req, res) {
- if (!req.user) {
- return res.sendStatus(403)
- }
var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
if (!audiobook) return res.sendStatus(404)
@@ -48,8 +45,8 @@ class BookController {
var hasUpdates = audiobook.update(req.body)
if (hasUpdates) {
await this.db.updateAudiobook(audiobook)
+ this.emitter('audiobook_updated', audiobook.toJSONExpanded())
}
- this.emitter('audiobook_updated', audiobook.toJSONExpanded())
res.json(audiobook.toJSON())
}
@@ -259,5 +256,71 @@ class BookController {
}
return this.cacheManager.handleCoverCache(res, audiobook, options)
}
+
+ // POST api/books/:id/match
+ async match(req, res) {
+ if (!req.user.canUpdate) {
+ Logger.warn('User attempted to match without permission', req.user)
+ return res.sendStatus(403)
+ }
+ var audiobook = this.db.audiobooks.find(a => a.id === req.params.id)
+ if (!audiobook || !audiobook.book.cover) return res.sendStatus(404)
+
+ // Check user can access this audiobooks library
+ if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) {
+ return res.sendStatus(403)
+ }
+
+ var options = req.body || {}
+ 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 res.json({
+ 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())
+ }
+
+ res.json({
+ updated: hasUpdated,
+ audiobook: audiobook.toJSONExpanded()
+ })
+ }
}
module.exports = new BookController()
\ No newline at end of file
diff --git a/server/objects/Library.js b/server/objects/Library.js
index f8dc30c9..e62c54e1 100644
--- a/server/objects/Library.js
+++ b/server/objects/Library.js
@@ -78,6 +78,10 @@ class Library {
this.name = payload.name
hasUpdates = true
}
+ if (payload.provider && payload.provider !== this.provider) {
+ this.provider = payload.provider
+ hasUpdates = true
+ }
if (!isNaN(payload.displayOrder) && payload.displayOrder !== this.displayOrder) {
this.displayOrder = Number(payload.displayOrder)
hasUpdates = true