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