mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:Library match all books #359
This commit is contained in:
		
							parent
							
								
									c953c3dee0
								
							
						
					
					
						commit
						11be49a535
					
				| @ -9,8 +9,11 @@ | ||||
|     </svg> | ||||
|     <p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p> | ||||
|     <div class="flex-grow" /> | ||||
|     <ui-btn v-show="isHovering && !libraryScan && canScan" small color="bg" @click.stop="scan">Scan</ui-btn> | ||||
|     <ui-btn v-show="isHovering && !libraryScan && canScan" small color="success" @click.stop="scan">Scan</ui-btn> | ||||
|     <ui-btn v-show="isHovering && !libraryScan && canScan" small color="bg" class="ml-2" @click.stop="forceScan">Force Re-Scan</ui-btn> | ||||
| 
 | ||||
|     <ui-btn v-show="isHovering && !libraryScan && canScan" small color="bg" class="ml-2" @click.stop="matchAll">Match Books</ui-btn> | ||||
| 
 | ||||
|     <span v-show="isHovering && !libraryScan && showEdit && canEdit" class="material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span> | ||||
|     <span v-show="!libraryScan && isHovering && showEdit && canDelete && !isDeleting" class="material-icons text-xl text-gray-300 ml-3" :class="isMain ? 'text-opacity-5 cursor-not-allowed' : 'hover:text-gray-50 cursor-pointer'" @click.stop="deleteClick">delete</span> | ||||
|     <div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin"> | ||||
| @ -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) | ||||
|     }, | ||||
|  | ||||
| @ -16,6 +16,8 @@ | ||||
|     <modals-edit-library-modal v-model="showLibraryModal" :library="selectedLibrary" /> | ||||
| 
 | ||||
|     <p class="text-xs mt-4 text-gray-200">*<strong>Force Re-Scan</strong> 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.</p> | ||||
| 
 | ||||
|     <p class="text-xs mt-4 text-gray-200">**<strong>Match Books</strong> 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.</p> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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 | ||||
| @ -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 | ||||
|  | ||||
| @ -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) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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) { | ||||
|  | ||||
| @ -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())) | ||||
|  | ||||
| @ -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 | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user