mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Fix:Quick match not removing empty series/authors #3743
This commit is contained in:
		
							parent
							
								
									7eb315a371
								
							
						
					
					
						commit
						5fa263023f
					
				| @ -54,8 +54,7 @@ export default { | ||||
|       options: { | ||||
|         provider: undefined, | ||||
|         overrideDetails: true, | ||||
|         overrideCover: true, | ||||
|         overrideDefaults: true | ||||
|         overrideCover: true | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| @ -99,8 +98,8 @@ export default { | ||||
|     init() { | ||||
|       // If we don't have a set provider (first open of dialog) or we've switched library, set | ||||
|       // the selected provider to the current library default provider | ||||
|       if (!this.options.provider || this.options.lastUsedLibrary != this.currentLibraryId) { | ||||
|         this.options.lastUsedLibrary = this.currentLibraryId | ||||
|       if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) { | ||||
|         this.lastUsedLibrary = this.currentLibraryId | ||||
|         this.options.provider = this.libraryProvider | ||||
|       } | ||||
|     }, | ||||
|  | ||||
| @ -1217,7 +1217,7 @@ class LibraryController { | ||||
|       Logger.error(`[LibraryController] Non-root user "${req.user.username}" attempted to match library items`) | ||||
|       return res.sendStatus(403) | ||||
|     } | ||||
|     Scanner.matchLibraryItems(req.library) | ||||
|     Scanner.matchLibraryItems(this, req.library) | ||||
|     res.sendStatus(200) | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -456,10 +456,24 @@ class LibraryItemController { | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async match(req, res) { | ||||
|     var libraryItem = req.libraryItem | ||||
|     const libraryItem = req.libraryItem | ||||
|     const reqBody = req.body || {} | ||||
| 
 | ||||
|     var options = req.body || {} | ||||
|     var matchResult = await Scanner.quickMatchLibraryItem(libraryItem, options) | ||||
|     const options = {} | ||||
|     const matchOptions = ['provider', 'title', 'author', 'isbn', 'asin'] | ||||
|     for (const key of matchOptions) { | ||||
|       if (reqBody[key] && typeof reqBody[key] === 'string') { | ||||
|         options[key] = reqBody[key] | ||||
|       } | ||||
|     } | ||||
|     if (reqBody.overrideCover !== undefined) { | ||||
|       options.overrideCover = !!reqBody.overrideCover | ||||
|     } | ||||
|     if (reqBody.overrideDetails !== undefined) { | ||||
|       options.overrideDetails = !!reqBody.overrideDetails | ||||
|     } | ||||
| 
 | ||||
|     var matchResult = await Scanner.quickMatchLibraryItem(this, libraryItem, options) | ||||
|     res.json(matchResult) | ||||
|   } | ||||
| 
 | ||||
| @ -642,7 +656,6 @@ class LibraryItemController { | ||||
|     let itemsUpdated = 0 | ||||
|     let itemsUnmatched = 0 | ||||
| 
 | ||||
|     const options = req.body.options || {} | ||||
|     if (!req.body.libraryItemIds?.length) { | ||||
|       return res.sendStatus(400) | ||||
|     } | ||||
| @ -656,8 +669,20 @@ class LibraryItemController { | ||||
| 
 | ||||
|     res.sendStatus(200) | ||||
| 
 | ||||
|     const reqBodyOptions = req.body.options || {} | ||||
|     const options = {} | ||||
|     if (reqBodyOptions.provider && typeof reqBodyOptions.provider === 'string') { | ||||
|       options.provider = reqBodyOptions.provider | ||||
|     } | ||||
|     if (reqBodyOptions.overrideCover !== undefined) { | ||||
|       options.overrideCover = !!reqBodyOptions.overrideCover | ||||
|     } | ||||
|     if (reqBodyOptions.overrideDetails !== undefined) { | ||||
|       options.overrideDetails = !!reqBodyOptions.overrideDetails | ||||
|     } | ||||
| 
 | ||||
|     for (const libraryItem of libraryItems) { | ||||
|       const matchResult = await Scanner.quickMatchLibraryItem(libraryItem, options) | ||||
|       const matchResult = await Scanner.quickMatchLibraryItem(this, libraryItem, options) | ||||
|       if (matchResult.updated) { | ||||
|         itemsUpdated++ | ||||
|       } else if (matchResult.warning) { | ||||
|  | ||||
| @ -342,7 +342,6 @@ class RssFeedManager { | ||||
|       } | ||||
|     }) | ||||
|     if (!feed) { | ||||
|       Logger.warn(`[RssFeedManager] closeFeedForEntityId: Feed not found for entity id ${entityId}`) | ||||
|       return false | ||||
|     } | ||||
|     return this.handleCloseFeed(feed) | ||||
|  | ||||
| @ -13,36 +13,58 @@ const LibraryScanner = require('./LibraryScanner') | ||||
| const CoverManager = require('../managers/CoverManager') | ||||
| const TaskManager = require('../managers/TaskManager') | ||||
| 
 | ||||
| /** | ||||
|  * @typedef QuickMatchOptions | ||||
|  * @property {string} [provider] | ||||
|  * @property {string} [title] | ||||
|  * @property {string} [author] | ||||
|  * @property {string} [isbn] - This override is currently unused in Abs clients | ||||
|  * @property {string} [asin] - This override is currently unused in Abs clients | ||||
|  * @property {boolean} [overrideCover] | ||||
|  * @property {boolean} [overrideDetails] | ||||
|  */ | ||||
| 
 | ||||
| class Scanner { | ||||
|   constructor() {} | ||||
| 
 | ||||
|   async quickMatchLibraryItem(libraryItem, options = {}) { | ||||
|     var provider = options.provider || 'google' | ||||
|     var searchTitle = options.title || libraryItem.media.metadata.title | ||||
|     var searchAuthor = options.author || libraryItem.media.metadata.authorName | ||||
|     var overrideDefaults = options.overrideDefaults || false | ||||
|   /** | ||||
|    * | ||||
|    * @param {import('../routers/ApiRouter')} apiRouterCtx | ||||
|    * @param {import('../objects/LibraryItem')} libraryItem | ||||
|    * @param {QuickMatchOptions} options | ||||
|    * @returns {Promise<{updated: boolean, libraryItem: import('../objects/LibraryItem')}>} | ||||
|    */ | ||||
|   async quickMatchLibraryItem(apiRouterCtx, libraryItem, options = {}) { | ||||
|     const provider = options.provider || 'google' | ||||
|     const searchTitle = options.title || libraryItem.media.metadata.title | ||||
|     const searchAuthor = options.author || libraryItem.media.metadata.authorName | ||||
| 
 | ||||
|     // Set to override existing metadata if scannerPreferMatchedMetadata setting is true and
 | ||||
|     // the overrideDefaults option is not set or set to false.
 | ||||
|     if (overrideDefaults == false && Database.serverSettings.scannerPreferMatchedMetadata) { | ||||
|     // If overrideCover and overrideDetails is not sent in options than use the server setting to determine if we should override
 | ||||
|     if (options.overrideCover === undefined && options.overrideDetails === undefined && Database.serverSettings.scannerPreferMatchedMetadata) { | ||||
|       options.overrideCover = true | ||||
|       options.overrideDetails = true | ||||
|     } | ||||
| 
 | ||||
|     var updatePayload = {} | ||||
|     var hasUpdated = false | ||||
|     let updatePayload = {} | ||||
|     let hasUpdated = false | ||||
| 
 | ||||
|     let existingAuthors = [] // Used for checking if authors or series are now empty
 | ||||
|     let existingSeries = [] | ||||
| 
 | ||||
|     if (libraryItem.isBook) { | ||||
|       var searchISBN = options.isbn || libraryItem.media.metadata.isbn | ||||
|       var searchASIN = options.asin || libraryItem.media.metadata.asin | ||||
|       existingAuthors = libraryItem.media.metadata.authors.map((a) => a.id) | ||||
|       existingSeries = libraryItem.media.metadata.series.map((s) => s.id) | ||||
| 
 | ||||
|       var results = await BookFinder.search(libraryItem, provider, searchTitle, searchAuthor, searchISBN, searchASIN, { maxFuzzySearches: 2 }) | ||||
|       const searchISBN = options.isbn || libraryItem.media.metadata.isbn | ||||
|       const searchASIN = options.asin || libraryItem.media.metadata.asin | ||||
| 
 | ||||
|       const results = await BookFinder.search(libraryItem, provider, searchTitle, searchAuthor, searchISBN, searchASIN, { maxFuzzySearches: 2 }) | ||||
|       if (!results.length) { | ||||
|         return { | ||||
|           warning: `No ${provider} match found` | ||||
|         } | ||||
|       } | ||||
|       var matchData = results[0] | ||||
|       const matchData = results[0] | ||||
| 
 | ||||
|       // Update cover if not set OR overrideCover flag
 | ||||
|       if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) { | ||||
| @ -58,13 +80,13 @@ class Scanner { | ||||
|       updatePayload = await this.quickMatchBookBuildUpdatePayload(libraryItem, matchData, options) | ||||
|     } else if (libraryItem.isPodcast) { | ||||
|       // Podcast quick match
 | ||||
|       var results = await PodcastFinder.search(searchTitle) | ||||
|       const results = await PodcastFinder.search(searchTitle) | ||||
|       if (!results.length) { | ||||
|         return { | ||||
|           warning: `No ${provider} match found` | ||||
|         } | ||||
|       } | ||||
|       var matchData = results[0] | ||||
|       const matchData = results[0] | ||||
| 
 | ||||
|       // Update cover if not set OR overrideCover flag
 | ||||
|       if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) { | ||||
| @ -95,6 +117,19 @@ class Scanner { | ||||
| 
 | ||||
|       await Database.updateLibraryItem(libraryItem) | ||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
| 
 | ||||
|       // Check if any authors or series are now empty and should be removed
 | ||||
|       if (libraryItem.isBook) { | ||||
|         const authorsRemoved = existingAuthors.filter((aid) => !libraryItem.media.metadata.authors.find((au) => au.id === aid)) | ||||
|         const seriesRemoved = existingSeries.filter((sid) => !libraryItem.media.metadata.series.find((se) => se.id === sid)) | ||||
| 
 | ||||
|         if (authorsRemoved.length) { | ||||
|           await apiRouterCtx.checkRemoveAuthorsWithNoBooks(authorsRemoved) | ||||
|         } | ||||
|         if (seriesRemoved.length) { | ||||
|           await apiRouterCtx.checkRemoveEmptySeries(seriesRemoved) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
| @ -149,6 +184,13 @@ class Scanner { | ||||
|     return updatePayload | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {import('../objects/LibraryItem')} libraryItem | ||||
|    * @param {*} matchData | ||||
|    * @param {QuickMatchOptions} options | ||||
|    * @returns | ||||
|    */ | ||||
|   async quickMatchBookBuildUpdatePayload(libraryItem, matchData, options) { | ||||
|     // Update media metadata if not set OR overrideDetails flag
 | ||||
|     const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'genres', 'tags', 'language', 'explicit', 'abridged', 'asin', 'isbn'] | ||||
| @ -307,12 +349,13 @@ class Scanner { | ||||
|   /** | ||||
|    * Quick match library items | ||||
|    * | ||||
|    * @param {import('../routers/ApiRouter')} apiRouterCtx | ||||
|    * @param {import('../models/Library')} library | ||||
|    * @param {import('../objects/LibraryItem')[]} libraryItems | ||||
|    * @param {LibraryScan} libraryScan | ||||
|    * @returns {Promise<boolean>} false if scan canceled | ||||
|    */ | ||||
|   async matchLibraryItemsChunk(library, libraryItems, libraryScan) { | ||||
|   async matchLibraryItemsChunk(apiRouterCtx, library, libraryItems, libraryScan) { | ||||
|     for (let i = 0; i < libraryItems.length; i++) { | ||||
|       const libraryItem = libraryItems[i] | ||||
| 
 | ||||
| @ -327,7 +370,7 @@ class Scanner { | ||||
|       } | ||||
| 
 | ||||
|       Logger.debug(`[Scanner] matchLibraryItems: Quick matching "${libraryItem.media.metadata.title}" (${i + 1} of ${libraryItems.length})`) | ||||
|       const result = await this.quickMatchLibraryItem(libraryItem, { provider: library.provider }) | ||||
|       const result = await this.quickMatchLibraryItem(apiRouterCtx, libraryItem, { provider: library.provider }) | ||||
|       if (result.warning) { | ||||
|         Logger.warn(`[Scanner] matchLibraryItems: Match warning ${result.warning} for library item "${libraryItem.media.metadata.title}"`) | ||||
|       } else if (result.updated) { | ||||
| @ -346,9 +389,10 @@ class Scanner { | ||||
|   /** | ||||
|    * Quick match all library items for library | ||||
|    * | ||||
|    * @param {import('../routers/ApiRouter')} apiRouterCtx | ||||
|    * @param {import('../models/Library')} library | ||||
|    */ | ||||
|   async matchLibraryItems(library) { | ||||
|   async matchLibraryItems(apiRouterCtx, library) { | ||||
|     if (library.mediaType === 'podcast') { | ||||
|       Logger.error(`[Scanner] matchLibraryItems: Match all not supported for podcasts yet`) | ||||
|       return | ||||
| @ -388,7 +432,7 @@ class Scanner { | ||||
|       hasMoreChunks = libraryItems.length === limit | ||||
|       let oldLibraryItems = libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li)) | ||||
| 
 | ||||
|       const shouldContinue = await this.matchLibraryItemsChunk(library, oldLibraryItems, libraryScan) | ||||
|       const shouldContinue = await this.matchLibraryItemsChunk(apiRouterCtx, library, oldLibraryItems, libraryScan) | ||||
|       if (!shouldContinue) { | ||||
|         isCanceled = true | ||||
|         break | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user