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: { |       options: { | ||||||
|         provider: undefined, |         provider: undefined, | ||||||
|         overrideDetails: true, |         overrideDetails: true, | ||||||
|         overrideCover: true, |         overrideCover: true | ||||||
|         overrideDefaults: true |  | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
| @ -99,8 +98,8 @@ export default { | |||||||
|     init() { |     init() { | ||||||
|       // If we don't have a set provider (first open of dialog) or we've switched library, set |       // 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 |       // the selected provider to the current library default provider | ||||||
|       if (!this.options.provider || this.options.lastUsedLibrary != this.currentLibraryId) { |       if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) { | ||||||
|         this.options.lastUsedLibrary = this.currentLibraryId |         this.lastUsedLibrary = this.currentLibraryId | ||||||
|         this.options.provider = this.libraryProvider |         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`) |       Logger.error(`[LibraryController] Non-root user "${req.user.username}" attempted to match library items`) | ||||||
|       return res.sendStatus(403) |       return res.sendStatus(403) | ||||||
|     } |     } | ||||||
|     Scanner.matchLibraryItems(req.library) |     Scanner.matchLibraryItems(this, req.library) | ||||||
|     res.sendStatus(200) |     res.sendStatus(200) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -456,10 +456,24 @@ class LibraryItemController { | |||||||
|    * @param {Response} res |    * @param {Response} res | ||||||
|    */ |    */ | ||||||
|   async match(req, res) { |   async match(req, res) { | ||||||
|     var libraryItem = req.libraryItem |     const libraryItem = req.libraryItem | ||||||
|  |     const reqBody = req.body || {} | ||||||
| 
 | 
 | ||||||
|     var options = req.body || {} |     const options = {} | ||||||
|     var matchResult = await Scanner.quickMatchLibraryItem(libraryItem, 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) |     res.json(matchResult) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @ -642,7 +656,6 @@ class LibraryItemController { | |||||||
|     let itemsUpdated = 0 |     let itemsUpdated = 0 | ||||||
|     let itemsUnmatched = 0 |     let itemsUnmatched = 0 | ||||||
| 
 | 
 | ||||||
|     const options = req.body.options || {} |  | ||||||
|     if (!req.body.libraryItemIds?.length) { |     if (!req.body.libraryItemIds?.length) { | ||||||
|       return res.sendStatus(400) |       return res.sendStatus(400) | ||||||
|     } |     } | ||||||
| @ -656,8 +669,20 @@ class LibraryItemController { | |||||||
| 
 | 
 | ||||||
|     res.sendStatus(200) |     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) { |     for (const libraryItem of libraryItems) { | ||||||
|       const matchResult = await Scanner.quickMatchLibraryItem(libraryItem, options) |       const matchResult = await Scanner.quickMatchLibraryItem(this, libraryItem, options) | ||||||
|       if (matchResult.updated) { |       if (matchResult.updated) { | ||||||
|         itemsUpdated++ |         itemsUpdated++ | ||||||
|       } else if (matchResult.warning) { |       } else if (matchResult.warning) { | ||||||
|  | |||||||
| @ -342,7 +342,6 @@ class RssFeedManager { | |||||||
|       } |       } | ||||||
|     }) |     }) | ||||||
|     if (!feed) { |     if (!feed) { | ||||||
|       Logger.warn(`[RssFeedManager] closeFeedForEntityId: Feed not found for entity id ${entityId}`) |  | ||||||
|       return false |       return false | ||||||
|     } |     } | ||||||
|     return this.handleCloseFeed(feed) |     return this.handleCloseFeed(feed) | ||||||
|  | |||||||
| @ -13,36 +13,58 @@ const LibraryScanner = require('./LibraryScanner') | |||||||
| const CoverManager = require('../managers/CoverManager') | const CoverManager = require('../managers/CoverManager') | ||||||
| const TaskManager = require('../managers/TaskManager') | 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 { | class Scanner { | ||||||
|   constructor() {} |   constructor() {} | ||||||
| 
 | 
 | ||||||
|   async quickMatchLibraryItem(libraryItem, options = {}) { |   /** | ||||||
|     var provider = options.provider || 'google' |    * | ||||||
|     var searchTitle = options.title || libraryItem.media.metadata.title |    * @param {import('../routers/ApiRouter')} apiRouterCtx | ||||||
|     var searchAuthor = options.author || libraryItem.media.metadata.authorName |    * @param {import('../objects/LibraryItem')} libraryItem | ||||||
|     var overrideDefaults = options.overrideDefaults || false |    * @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
 |     // If overrideCover and overrideDetails is not sent in options than use the server setting to determine if we should override
 | ||||||
|     // the overrideDefaults option is not set or set to false.
 |     if (options.overrideCover === undefined && options.overrideDetails === undefined && Database.serverSettings.scannerPreferMatchedMetadata) { | ||||||
|     if (overrideDefaults == false && Database.serverSettings.scannerPreferMatchedMetadata) { |  | ||||||
|       options.overrideCover = true |       options.overrideCover = true | ||||||
|       options.overrideDetails = true |       options.overrideDetails = true | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     var updatePayload = {} |     let updatePayload = {} | ||||||
|     var hasUpdated = false |     let hasUpdated = false | ||||||
|  | 
 | ||||||
|  |     let existingAuthors = [] // Used for checking if authors or series are now empty
 | ||||||
|  |     let existingSeries = [] | ||||||
| 
 | 
 | ||||||
|     if (libraryItem.isBook) { |     if (libraryItem.isBook) { | ||||||
|       var searchISBN = options.isbn || libraryItem.media.metadata.isbn |       existingAuthors = libraryItem.media.metadata.authors.map((a) => a.id) | ||||||
|       var searchASIN = options.asin || libraryItem.media.metadata.asin |       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) { |       if (!results.length) { | ||||||
|         return { |         return { | ||||||
|           warning: `No ${provider} match found` |           warning: `No ${provider} match found` | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       var matchData = results[0] |       const matchData = results[0] | ||||||
| 
 | 
 | ||||||
|       // Update cover if not set OR overrideCover flag
 |       // Update cover if not set OR overrideCover flag
 | ||||||
|       if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) { |       if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) { | ||||||
| @ -58,13 +80,13 @@ class Scanner { | |||||||
|       updatePayload = await this.quickMatchBookBuildUpdatePayload(libraryItem, matchData, options) |       updatePayload = await this.quickMatchBookBuildUpdatePayload(libraryItem, matchData, options) | ||||||
|     } else if (libraryItem.isPodcast) { |     } else if (libraryItem.isPodcast) { | ||||||
|       // Podcast quick match
 |       // Podcast quick match
 | ||||||
|       var results = await PodcastFinder.search(searchTitle) |       const results = await PodcastFinder.search(searchTitle) | ||||||
|       if (!results.length) { |       if (!results.length) { | ||||||
|         return { |         return { | ||||||
|           warning: `No ${provider} match found` |           warning: `No ${provider} match found` | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       var matchData = results[0] |       const matchData = results[0] | ||||||
| 
 | 
 | ||||||
|       // Update cover if not set OR overrideCover flag
 |       // Update cover if not set OR overrideCover flag
 | ||||||
|       if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) { |       if (matchData.cover && (!libraryItem.media.coverPath || options.overrideCover)) { | ||||||
| @ -95,6 +117,19 @@ class Scanner { | |||||||
| 
 | 
 | ||||||
|       await Database.updateLibraryItem(libraryItem) |       await Database.updateLibraryItem(libraryItem) | ||||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) |       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 { |     return { | ||||||
| @ -149,6 +184,13 @@ class Scanner { | |||||||
|     return updatePayload |     return updatePayload | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * @param {import('../objects/LibraryItem')} libraryItem | ||||||
|  |    * @param {*} matchData | ||||||
|  |    * @param {QuickMatchOptions} options | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|   async quickMatchBookBuildUpdatePayload(libraryItem, matchData, options) { |   async quickMatchBookBuildUpdatePayload(libraryItem, matchData, options) { | ||||||
|     // Update media metadata if not set OR overrideDetails flag
 |     // Update media metadata if not set OR overrideDetails flag
 | ||||||
|     const detailKeysToUpdate = ['title', 'subtitle', 'description', 'narrator', 'publisher', 'publishedYear', 'genres', 'tags', 'language', 'explicit', 'abridged', 'asin', 'isbn'] |     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 |    * Quick match library items | ||||||
|    * |    * | ||||||
|  |    * @param {import('../routers/ApiRouter')} apiRouterCtx | ||||||
|    * @param {import('../models/Library')} library |    * @param {import('../models/Library')} library | ||||||
|    * @param {import('../objects/LibraryItem')[]} libraryItems |    * @param {import('../objects/LibraryItem')[]} libraryItems | ||||||
|    * @param {LibraryScan} libraryScan |    * @param {LibraryScan} libraryScan | ||||||
|    * @returns {Promise<boolean>} false if scan canceled |    * @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++) { |     for (let i = 0; i < libraryItems.length; i++) { | ||||||
|       const libraryItem = libraryItems[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})`) |       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) { |       if (result.warning) { | ||||||
|         Logger.warn(`[Scanner] matchLibraryItems: Match warning ${result.warning} for library item "${libraryItem.media.metadata.title}"`) |         Logger.warn(`[Scanner] matchLibraryItems: Match warning ${result.warning} for library item "${libraryItem.media.metadata.title}"`) | ||||||
|       } else if (result.updated) { |       } else if (result.updated) { | ||||||
| @ -346,9 +389,10 @@ class Scanner { | |||||||
|   /** |   /** | ||||||
|    * Quick match all library items for library |    * Quick match all library items for library | ||||||
|    * |    * | ||||||
|  |    * @param {import('../routers/ApiRouter')} apiRouterCtx | ||||||
|    * @param {import('../models/Library')} library |    * @param {import('../models/Library')} library | ||||||
|    */ |    */ | ||||||
|   async matchLibraryItems(library) { |   async matchLibraryItems(apiRouterCtx, library) { | ||||||
|     if (library.mediaType === 'podcast') { |     if (library.mediaType === 'podcast') { | ||||||
|       Logger.error(`[Scanner] matchLibraryItems: Match all not supported for podcasts yet`) |       Logger.error(`[Scanner] matchLibraryItems: Match all not supported for podcasts yet`) | ||||||
|       return |       return | ||||||
| @ -388,7 +432,7 @@ class Scanner { | |||||||
|       hasMoreChunks = libraryItems.length === limit |       hasMoreChunks = libraryItems.length === limit | ||||||
|       let oldLibraryItems = libraryItems.map((li) => Database.libraryItemModel.getOldLibraryItem(li)) |       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) { |       if (!shouldContinue) { | ||||||
|         isCanceled = true |         isCanceled = true | ||||||
|         break |         break | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user