mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Remove old Author object & fix issue deleting empty authors
This commit is contained in:
		
							parent
							
								
									acc4bdbc5e
								
							
						
					
					
						commit
						ba742563c2
					
				| @ -462,26 +462,6 @@ class Database { | ||||
|     await this.models.series.removeById(seriesId) | ||||
|   } | ||||
| 
 | ||||
|   async createAuthor(oldAuthor) { | ||||
|     if (!this.sequelize) return false | ||||
|     await this.models.author.createFromOld(oldAuthor) | ||||
|   } | ||||
| 
 | ||||
|   async createBulkAuthors(oldAuthors) { | ||||
|     if (!this.sequelize) return false | ||||
|     await this.models.author.createBulkFromOld(oldAuthors) | ||||
|   } | ||||
| 
 | ||||
|   updateAuthor(oldAuthor) { | ||||
|     if (!this.sequelize) return false | ||||
|     return this.models.author.updateFromOld(oldAuthor) | ||||
|   } | ||||
| 
 | ||||
|   async removeAuthor(authorId) { | ||||
|     if (!this.sequelize) return false | ||||
|     await this.models.author.removeById(authorId) | ||||
|   } | ||||
| 
 | ||||
|   async createBulkBookAuthors(bookAuthors) { | ||||
|     if (!this.sequelize) return false | ||||
|     await this.models.bookAuthor.bulkCreate(bookAuthors) | ||||
| @ -684,7 +664,7 @@ class Database { | ||||
|    */ | ||||
|   async getAuthorIdByName(libraryId, authorName) { | ||||
|     if (!this.libraryFilterData[libraryId]) { | ||||
|       return (await this.authorModel.getOldByNameAndLibrary(authorName, libraryId))?.id || null | ||||
|       return (await this.authorModel.getByNameAndLibrary(authorName, libraryId))?.id || null | ||||
|     } | ||||
|     return this.libraryFilterData[libraryId].authors.find((au) => au.name === authorName)?.id || null | ||||
|   } | ||||
|  | ||||
| @ -21,6 +21,11 @@ const naturalSort = createNewSortInstance({ | ||||
|  * @property {import('../models/User')} user | ||||
|  * | ||||
|  * @typedef {Request & RequestUserObject} RequestWithUser | ||||
|  * | ||||
|  * @typedef RequestEntityObject | ||||
|  * @property {import('../models/Author')} author | ||||
|  * | ||||
|  * @typedef {RequestWithUser & RequestEntityObject} AuthorControllerRequest | ||||
|  */ | ||||
| 
 | ||||
| class AuthorController { | ||||
| @ -29,13 +34,13 @@ class AuthorController { | ||||
|   /** | ||||
|    * GET: /api/authors/:id | ||||
|    * | ||||
|    * @param {RequestWithUser} req | ||||
|    * @param {AuthorControllerRequest} req | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async findOne(req, res) { | ||||
|     const include = (req.query.include || '').split(',') | ||||
| 
 | ||||
|     const authorJson = req.author.toJSON() | ||||
|     const authorJson = req.author.toOldJSON() | ||||
| 
 | ||||
|     // Used on author landing page to include library items and items grouped in series
 | ||||
|     if (include.includes('items')) { | ||||
| @ -80,25 +85,30 @@ class AuthorController { | ||||
|   /** | ||||
|    * PATCH: /api/authors/:id | ||||
|    * | ||||
|    * @param {RequestWithUser} req | ||||
|    * @param {AuthorControllerRequest} req | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async update(req, res) { | ||||
|     const payload = req.body | ||||
|     let hasUpdated = false | ||||
| 
 | ||||
|     // author imagePath must be set through other endpoints as of v2.4.5
 | ||||
|     if (payload.imagePath !== undefined) { | ||||
|       Logger.warn(`[AuthorController] Updating local author imagePath is not supported`) | ||||
|       delete payload.imagePath | ||||
|     const keysToUpdate = ['name', 'description', 'asin'] | ||||
|     const payload = {} | ||||
|     for (const key in req.body) { | ||||
|       if (keysToUpdate.includes(key) && (typeof req.body[key] === 'string' || req.body[key] === null)) { | ||||
|         payload[key] = req.body[key] | ||||
|       } | ||||
|     } | ||||
|     if (!Object.keys(payload).length) { | ||||
|       Logger.error(`[AuthorController] Invalid request payload. No valid keys found`, req.body) | ||||
|       return res.status(400).send('Invalid request payload. No valid keys found') | ||||
|     } | ||||
| 
 | ||||
|     let hasUpdated = false | ||||
| 
 | ||||
|     const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name | ||||
| 
 | ||||
|     // Check if author name matches another author and merge the authors
 | ||||
|     let existingAuthor = null | ||||
|     if (authorNameUpdate) { | ||||
|       const author = await Database.authorModel.findOne({ | ||||
|       existingAuthor = await Database.authorModel.findOne({ | ||||
|         where: { | ||||
|           id: { | ||||
|             [sequelize.Op.not]: req.author.id | ||||
| @ -106,7 +116,6 @@ class AuthorController { | ||||
|           name: payload.name | ||||
|         } | ||||
|       }) | ||||
|       existingAuthor = author?.getOldAuthor() | ||||
|     } | ||||
|     if (existingAuthor) { | ||||
|       Logger.info(`[AuthorController] Merging author "${req.author.name}" with "${existingAuthor.name}"`) | ||||
| @ -143,86 +152,87 @@ class AuthorController { | ||||
|       } | ||||
| 
 | ||||
|       // Remove old author
 | ||||
|       await Database.removeAuthor(req.author.id) | ||||
|       SocketAuthority.emitter('author_removed', req.author.toJSON()) | ||||
|       const oldAuthorJSON = req.author.toOldJSON() | ||||
|       await req.author.destroy() | ||||
|       SocketAuthority.emitter('author_removed', oldAuthorJSON) | ||||
|       // Update filter data
 | ||||
|       Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id) | ||||
|       Database.removeAuthorFromFilterData(oldAuthorJSON.libraryId, oldAuthorJSON.id) | ||||
| 
 | ||||
|       // Send updated num books for merged author
 | ||||
|       const numBooks = await Database.bookAuthorModel.getCountForAuthor(existingAuthor.id) | ||||
|       SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks)) | ||||
|       SocketAuthority.emitter('author_updated', existingAuthor.toOldJSONExpanded(numBooks)) | ||||
| 
 | ||||
|       res.json({ | ||||
|         author: existingAuthor.toJSON(), | ||||
|         author: existingAuthor.toOldJSON(), | ||||
|         merged: true | ||||
|       }) | ||||
|     } else { | ||||
|       // Regular author update
 | ||||
|       if (req.author.update(payload)) { | ||||
|         hasUpdated = true | ||||
|       } | ||||
|       return | ||||
|     } | ||||
| 
 | ||||
|       if (hasUpdated) { | ||||
|         req.author.updatedAt = Date.now() | ||||
|     // Regular author update
 | ||||
|     req.author.set(payload) | ||||
|     if (req.author.changed()) { | ||||
|       await req.author.save() | ||||
|       hasUpdated = true | ||||
|     } | ||||
| 
 | ||||
|         let numBooksForAuthor = 0 | ||||
|         if (authorNameUpdate) { | ||||
|           const allItemsWithAuthor = await Database.authorModel.getAllLibraryItemsForAuthor(req.author.id) | ||||
|     if (hasUpdated) { | ||||
|       let numBooksForAuthor = 0 | ||||
|       if (authorNameUpdate) { | ||||
|         const allItemsWithAuthor = await Database.authorModel.getAllLibraryItemsForAuthor(req.author.id) | ||||
| 
 | ||||
|           numBooksForAuthor = allItemsWithAuthor.length | ||||
|           const oldLibraryItems = [] | ||||
|           // Update author name on all books
 | ||||
|           for (const libraryItem of allItemsWithAuthor) { | ||||
|             libraryItem.media.authors = libraryItem.media.authors.map((au) => { | ||||
|               if (au.id === req.author.id) { | ||||
|                 au.name = req.author.name | ||||
|               } | ||||
|               return au | ||||
|             }) | ||||
|             const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem) | ||||
|             oldLibraryItems.push(oldLibraryItem) | ||||
|         numBooksForAuthor = allItemsWithAuthor.length | ||||
|         const oldLibraryItems = [] | ||||
|         // Update author name on all books
 | ||||
|         for (const libraryItem of allItemsWithAuthor) { | ||||
|           libraryItem.media.authors = libraryItem.media.authors.map((au) => { | ||||
|             if (au.id === req.author.id) { | ||||
|               au.name = req.author.name | ||||
|             } | ||||
|             return au | ||||
|           }) | ||||
|           const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem) | ||||
|           oldLibraryItems.push(oldLibraryItem) | ||||
| 
 | ||||
|             await libraryItem.saveMetadataFile() | ||||
|           } | ||||
| 
 | ||||
|           if (oldLibraryItems.length) { | ||||
|             SocketAuthority.emitter( | ||||
|               'items_updated', | ||||
|               oldLibraryItems.map((li) => li.toJSONExpanded()) | ||||
|             ) | ||||
|           } | ||||
|         } else { | ||||
|           numBooksForAuthor = await Database.bookAuthorModel.getCountForAuthor(req.author.id) | ||||
|           await libraryItem.saveMetadataFile() | ||||
|         } | ||||
| 
 | ||||
|         await Database.updateAuthor(req.author) | ||||
|         SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooksForAuthor)) | ||||
|         if (oldLibraryItems.length) { | ||||
|           SocketAuthority.emitter( | ||||
|             'items_updated', | ||||
|             oldLibraryItems.map((li) => li.toJSONExpanded()) | ||||
|           ) | ||||
|         } | ||||
|       } else { | ||||
|         numBooksForAuthor = await Database.bookAuthorModel.getCountForAuthor(req.author.id) | ||||
|       } | ||||
| 
 | ||||
|       res.json({ | ||||
|         author: req.author.toJSON(), | ||||
|         updated: hasUpdated | ||||
|       }) | ||||
|       SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooksForAuthor)) | ||||
|     } | ||||
| 
 | ||||
|     res.json({ | ||||
|       author: req.author.toOldJSON(), | ||||
|       updated: hasUpdated | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * DELETE: /api/authors/:id | ||||
|    * Remove author from all books and delete | ||||
|    * | ||||
|    * @param {RequestWithUser} req | ||||
|    * @param {AuthorControllerRequest} req | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async delete(req, res) { | ||||
|     Logger.info(`[AuthorController] Removing author "${req.author.name}"`) | ||||
| 
 | ||||
|     await Database.authorModel.removeById(req.author.id) | ||||
| 
 | ||||
|     if (req.author.imagePath) { | ||||
|       await CacheManager.purgeImageCache(req.author.id) // Purge cache
 | ||||
|     } | ||||
| 
 | ||||
|     SocketAuthority.emitter('author_removed', req.author.toJSON()) | ||||
|     await req.author.destroy() | ||||
| 
 | ||||
|     SocketAuthority.emitter('author_removed', req.author.toOldJSON()) | ||||
| 
 | ||||
|     // Update filter data
 | ||||
|     Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id) | ||||
| @ -234,7 +244,7 @@ class AuthorController { | ||||
|    * POST: /api/authors/:id/image | ||||
|    * Upload author image from web URL | ||||
|    * | ||||
|    * @param {RequestWithUser} req | ||||
|    * @param {AuthorControllerRequest} req | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async uploadImage(req, res) { | ||||
| @ -265,13 +275,14 @@ class AuthorController { | ||||
|     } | ||||
| 
 | ||||
|     req.author.imagePath = result.path | ||||
|     req.author.updatedAt = Date.now() | ||||
|     await Database.authorModel.updateFromOld(req.author) | ||||
|     // imagePath may not have changed, but we still want to update the updatedAt field to bust image cache
 | ||||
|     req.author.changed('imagePath', true) | ||||
|     await req.author.save() | ||||
| 
 | ||||
|     const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id) | ||||
|     SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) | ||||
|     SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooks)) | ||||
|     res.json({ | ||||
|       author: req.author.toJSON() | ||||
|       author: req.author.toOldJSON() | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
| @ -279,7 +290,7 @@ class AuthorController { | ||||
|    * DELETE: /api/authors/:id/image | ||||
|    * Remove author image & delete image file | ||||
|    * | ||||
|    * @param {RequestWithUser} req | ||||
|    * @param {AuthorControllerRequest} req | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async deleteImage(req, res) { | ||||
| @ -291,19 +302,19 @@ class AuthorController { | ||||
|     await CacheManager.purgeImageCache(req.author.id) // Purge cache
 | ||||
|     await CoverManager.removeFile(req.author.imagePath) | ||||
|     req.author.imagePath = null | ||||
|     await Database.authorModel.updateFromOld(req.author) | ||||
|     await req.author.save() | ||||
| 
 | ||||
|     const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id) | ||||
|     SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) | ||||
|     SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooks)) | ||||
|     res.json({ | ||||
|       author: req.author.toJSON() | ||||
|       author: req.author.toOldJSON() | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * POST: /api/authors/:id/match | ||||
|    * | ||||
|    * @param {RequestWithUser} req | ||||
|    * @param {AuthorControllerRequest} req | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async match(req, res) { | ||||
| @ -342,24 +353,22 @@ class AuthorController { | ||||
|     } | ||||
| 
 | ||||
|     if (hasUpdates) { | ||||
|       req.author.updatedAt = Date.now() | ||||
| 
 | ||||
|       await Database.updateAuthor(req.author) | ||||
|       await req.author.save() | ||||
| 
 | ||||
|       const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id) | ||||
|       SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooks)) | ||||
|       SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooks)) | ||||
|     } | ||||
| 
 | ||||
|     res.json({ | ||||
|       updated: hasUpdates, | ||||
|       author: req.author | ||||
|       author: req.author.toOldJSON() | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * GET: /api/authors/:id/image | ||||
|    * | ||||
|    * @param {RequestWithUser} req | ||||
|    * @param {AuthorControllerRequest} req | ||||
|    * @param {Response} res | ||||
|    */ | ||||
|   async getImage(req, res) { | ||||
| @ -392,7 +401,7 @@ class AuthorController { | ||||
|    * @param {NextFunction} next | ||||
|    */ | ||||
|   async middleware(req, res, next) { | ||||
|     const author = await Database.authorModel.getOldById(req.params.id) | ||||
|     const author = await Database.authorModel.findByPk(req.params.id) | ||||
|     if (!author) return res.sendStatus(404) | ||||
| 
 | ||||
|     if (req.method == 'DELETE' && !req.user.canDelete) { | ||||
|  | ||||
| @ -887,8 +887,7 @@ class LibraryController { | ||||
|     const oldAuthors = [] | ||||
| 
 | ||||
|     for (const author of authors) { | ||||
|       const oldAuthor = author.getOldAuthor().toJSON() | ||||
|       oldAuthor.numBooks = author.books.length | ||||
|       const oldAuthor = author.toOldJSONExpanded(author.books.length) | ||||
|       oldAuthor.lastFirst = author.lastFirst | ||||
|       oldAuthors.push(oldAuthor) | ||||
|     } | ||||
|  | ||||
| @ -151,6 +151,8 @@ class LibraryItemController { | ||||
|    * PATCH: /items/:id/media | ||||
|    * Update media for a library item. Will create new authors & series when necessary | ||||
|    * | ||||
|    * @this {import('../routers/ApiRouter')} | ||||
|    * | ||||
|    * @param {RequestWithUser} req | ||||
|    * @param {Response} res | ||||
|    */ | ||||
| @ -185,6 +187,12 @@ class LibraryItemController { | ||||
|       seriesRemoved = libraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id)) | ||||
|     } | ||||
| 
 | ||||
|     let authorsRemoved = [] | ||||
|     if (libraryItem.isBook && mediaPayload.metadata?.authors) { | ||||
|       const authorIdsInUpdate = mediaPayload.metadata.authors.map((au) => au.id) | ||||
|       authorsRemoved = libraryItem.media.metadata.authors.filter((au) => !authorIdsInUpdate.includes(au.id)) | ||||
|     } | ||||
| 
 | ||||
|     const hasUpdates = libraryItem.media.update(mediaPayload) || mediaPayload.url | ||||
|     if (hasUpdates) { | ||||
|       libraryItem.updatedAt = Date.now() | ||||
| @ -205,6 +213,15 @@ class LibraryItemController { | ||||
|       Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`) | ||||
|       await Database.updateLibraryItem(libraryItem) | ||||
|       SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded()) | ||||
| 
 | ||||
|       if (authorsRemoved.length) { | ||||
|         // Check remove empty authors
 | ||||
|         Logger.debug(`[LibraryItemController] Authors were removed from book. Check if authors are now empty.`) | ||||
|         await this.checkRemoveAuthorsWithNoBooks( | ||||
|           libraryItem.libraryId, | ||||
|           authorsRemoved.map((au) => au.id) | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|     res.json({ | ||||
|       updated: hasUpdates, | ||||
| @ -823,7 +840,7 @@ class LibraryItemController { | ||||
|       // We actually need to check for Webkit on Apple mobile devices because this issue impacts all browsers on iOS/iPadOS/etc, not just Safari.
 | ||||
|       const isAppleMobileBrowser = ua.device.vendor === 'Apple' && ua.device.type === 'mobile' && ua.engine.name === 'WebKit' | ||||
|       if (isAppleMobileBrowser && audioMimeType === AudioMimeType.M4B) { | ||||
|           audioMimeType = 'audio/m4b' | ||||
|         audioMimeType = 'audio/m4b' | ||||
|       } | ||||
|       res.setHeader('Content-Type', audioMimeType) | ||||
|     } | ||||
|  | ||||
| @ -124,6 +124,13 @@ class CacheManager { | ||||
|     await this.ensureCachePaths() | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {import('express').Response} res | ||||
|    * @param {import('../models/Author')} author | ||||
|    * @param {{ format?: string, width?: number, height?: number }} options | ||||
|    * @returns | ||||
|    */ | ||||
|   async handleAuthorCache(res, author, options = {}) { | ||||
|     const format = options.format || 'webp' | ||||
|     const width = options.width || 400 | ||||
|  | ||||
| @ -1,7 +1,5 @@ | ||||
| const { DataTypes, Model, where, fn, col } = require('sequelize') | ||||
| 
 | ||||
| const oldAuthor = require('../objects/entities/Author') | ||||
| 
 | ||||
| class Author extends Model { | ||||
|   constructor(values, options) { | ||||
|     super(values, options) | ||||
| @ -26,69 +24,6 @@ class Author extends Model { | ||||
|     this.createdAt | ||||
|   } | ||||
| 
 | ||||
|   getOldAuthor() { | ||||
|     return new oldAuthor({ | ||||
|       id: this.id, | ||||
|       asin: this.asin, | ||||
|       name: this.name, | ||||
|       description: this.description, | ||||
|       imagePath: this.imagePath, | ||||
|       libraryId: this.libraryId, | ||||
|       addedAt: this.createdAt.valueOf(), | ||||
|       updatedAt: this.updatedAt.valueOf() | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   static updateFromOld(oldAuthor) { | ||||
|     const author = this.getFromOld(oldAuthor) | ||||
|     return this.update(author, { | ||||
|       where: { | ||||
|         id: author.id | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   static createFromOld(oldAuthor) { | ||||
|     const author = this.getFromOld(oldAuthor) | ||||
|     return this.create(author) | ||||
|   } | ||||
| 
 | ||||
|   static createBulkFromOld(oldAuthors) { | ||||
|     const authors = oldAuthors.map(this.getFromOld) | ||||
|     return this.bulkCreate(authors) | ||||
|   } | ||||
| 
 | ||||
|   static getFromOld(oldAuthor) { | ||||
|     return { | ||||
|       id: oldAuthor.id, | ||||
|       name: oldAuthor.name, | ||||
|       lastFirst: oldAuthor.lastFirst, | ||||
|       asin: oldAuthor.asin, | ||||
|       description: oldAuthor.description, | ||||
|       imagePath: oldAuthor.imagePath, | ||||
|       libraryId: oldAuthor.libraryId | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   static removeById(authorId) { | ||||
|     return this.destroy({ | ||||
|       where: { | ||||
|         id: authorId | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get oldAuthor by id | ||||
|    * @param {string} authorId | ||||
|    * @returns {Promise<oldAuthor>} | ||||
|    */ | ||||
|   static async getOldById(authorId) { | ||||
|     const author = await this.findByPk(authorId) | ||||
|     if (!author) return null | ||||
|     return author.getOldAuthor() | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Check if author exists | ||||
|    * @param {string} authorId | ||||
| @ -99,25 +34,22 @@ class Author extends Model { | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get old author by name and libraryId. name case insensitive | ||||
|    * Get author by name and libraryId. name case insensitive | ||||
|    * TODO: Look for authors ignoring punctuation | ||||
|    * | ||||
|    * @param {string} authorName | ||||
|    * @param {string} libraryId | ||||
|    * @returns {Promise<oldAuthor>} | ||||
|    * @returns {Promise<Author>} | ||||
|    */ | ||||
|   static async getOldByNameAndLibrary(authorName, libraryId) { | ||||
|     const author = ( | ||||
|       await this.findOne({ | ||||
|         where: [ | ||||
|           where(fn('lower', col('name')), authorName.toLowerCase()), | ||||
|           { | ||||
|             libraryId | ||||
|           } | ||||
|         ] | ||||
|       }) | ||||
|     )?.getOldAuthor() | ||||
|     return author | ||||
|   static async getByNameAndLibrary(authorName, libraryId) { | ||||
|     return this.findOne({ | ||||
|       where: [ | ||||
|         where(fn('lower', col('name')), authorName.toLowerCase()), | ||||
|         { | ||||
|           libraryId | ||||
|         } | ||||
|       ] | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
| @ -213,5 +145,36 @@ class Author extends Model { | ||||
|     }) | ||||
|     Author.belongsTo(library) | ||||
|   } | ||||
| 
 | ||||
|   toOldJSON() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       asin: this.asin, | ||||
|       name: this.name, | ||||
|       description: this.description, | ||||
|       imagePath: this.imagePath, | ||||
|       libraryId: this.libraryId, | ||||
|       addedAt: this.createdAt.valueOf(), | ||||
|       updatedAt: this.updatedAt.valueOf() | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * | ||||
|    * @param {number} numBooks | ||||
|    * @returns | ||||
|    */ | ||||
|   toOldJSONExpanded(numBooks = 0) { | ||||
|     const oldJson = this.toOldJSON() | ||||
|     oldJson.numBooks = numBooks | ||||
|     return oldJson | ||||
|   } | ||||
| 
 | ||||
|   toJSONMinimal() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       name: this.name | ||||
|     } | ||||
|   } | ||||
| } | ||||
| module.exports = Author | ||||
|  | ||||
| @ -773,7 +773,7 @@ class LibraryItem extends Model { | ||||
| 
 | ||||
|   /** | ||||
|    * Get book library items for author, optional use user permissions | ||||
|    * @param {oldAuthor} author | ||||
|    * @param {import('./Author')} author | ||||
|    * @param {import('./User')} user | ||||
|    * @returns {Promise<oldLibraryItem[]>} | ||||
|    */ | ||||
|  | ||||
| @ -1,101 +0,0 @@ | ||||
| const Logger = require('../../Logger') | ||||
| const uuidv4 = require("uuid").v4 | ||||
| const { checkNamesAreEqual, nameToLastFirst } = require('../../utils/parsers/parseNameString') | ||||
| 
 | ||||
| class Author { | ||||
|   constructor(author) { | ||||
|     this.id = null | ||||
|     this.asin = null | ||||
|     this.name = null | ||||
|     this.description = null | ||||
|     this.imagePath = null | ||||
|     this.addedAt = null | ||||
|     this.updatedAt = null | ||||
|     this.libraryId = null | ||||
| 
 | ||||
|     if (author) { | ||||
|       this.construct(author) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   construct(author) { | ||||
|     this.id = author.id | ||||
|     this.asin = author.asin | ||||
|     this.name = author.name || '' | ||||
|     this.description = author.description || null | ||||
|     this.imagePath = author.imagePath | ||||
|     this.addedAt = author.addedAt | ||||
|     this.updatedAt = author.updatedAt | ||||
|     this.libraryId = author.libraryId | ||||
|   } | ||||
| 
 | ||||
|   get lastFirst() { | ||||
|     if (!this.name) return '' | ||||
|     return nameToLastFirst(this.name) | ||||
|   } | ||||
| 
 | ||||
|   toJSON() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       asin: this.asin, | ||||
|       name: this.name, | ||||
|       description: this.description, | ||||
|       imagePath: this.imagePath, | ||||
|       addedAt: this.addedAt, | ||||
|       updatedAt: this.updatedAt, | ||||
|       libraryId: this.libraryId | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   toJSONExpanded(numBooks = 0) { | ||||
|     const json = this.toJSON() | ||||
|     json.numBooks = numBooks | ||||
|     return json | ||||
|   } | ||||
| 
 | ||||
|   toJSONMinimal() { | ||||
|     return { | ||||
|       id: this.id, | ||||
|       name: this.name | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setData(data, libraryId) { | ||||
|     this.id = uuidv4() | ||||
|     if (!data.name) { | ||||
|       Logger.error(`[Author] setData: Setting author data without a name`, data) | ||||
|     } | ||||
|     this.name = data.name || '' | ||||
|     this.description = data.description || null | ||||
|     this.asin = data.asin || null | ||||
|     this.imagePath = data.imagePath || null | ||||
|     this.addedAt = Date.now() | ||||
|     this.updatedAt = Date.now() | ||||
|     this.libraryId = libraryId | ||||
|   } | ||||
| 
 | ||||
|   update(payload) { | ||||
|     const json = this.toJSON() | ||||
|     delete json.id | ||||
|     delete json.addedAt | ||||
|     delete json.updatedAt | ||||
|     let hasUpdates = false | ||||
|     for (const key in json) { | ||||
|       if (payload[key] !== undefined && json[key] != payload[key]) { | ||||
|         this[key] = payload[key] | ||||
|         hasUpdates = true | ||||
|       } | ||||
|     } | ||||
|     return hasUpdates | ||||
|   } | ||||
| 
 | ||||
|   checkNameEquals(name) { | ||||
|     if (!name) return false | ||||
|     if (this.name === null) { | ||||
|       Logger.error(`[Author] Author name is null (${this.id})`) | ||||
|       return false | ||||
|     } | ||||
|     return checkNamesAreEqual(this.name, name) | ||||
|   } | ||||
| } | ||||
| module.exports = Author | ||||
| @ -1,5 +1,6 @@ | ||||
| const express = require('express') | ||||
| const Path = require('path') | ||||
| const sequelize = require('sequelize') | ||||
| 
 | ||||
| const Logger = require('../Logger') | ||||
| const Database = require('../Database') | ||||
| @ -32,7 +33,6 @@ const CustomMetadataProviderController = require('../controllers/CustomMetadataP | ||||
| const MiscController = require('../controllers/MiscController') | ||||
| const ShareController = require('../controllers/ShareController') | ||||
| 
 | ||||
| const Author = require('../objects/entities/Author') | ||||
| const Series = require('../objects/entities/Series') | ||||
| 
 | ||||
| class ApiRouter { | ||||
| @ -469,6 +469,54 @@ class ApiRouter { | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Remove authors with no books and unset asin, description and imagePath | ||||
|    * Note: Other implementation is in BookScanner.checkAuthorsRemovedFromBooks (can be merged) | ||||
|    * | ||||
|    * @param {string} libraryId | ||||
|    * @param {string[]} authorIds | ||||
|    * @returns {Promise<void>} | ||||
|    */ | ||||
|   async checkRemoveAuthorsWithNoBooks(libraryId, authorIds) { | ||||
|     if (!authorIds?.length) return | ||||
| 
 | ||||
|     const bookAuthorsToRemove = ( | ||||
|       await Database.authorModel.findAll({ | ||||
|         where: [ | ||||
|           { | ||||
|             id: authorIds, | ||||
|             asin: { | ||||
|               [sequelize.Op.or]: [null, ''] | ||||
|             }, | ||||
|             description: { | ||||
|               [sequelize.Op.or]: [null, ''] | ||||
|             }, | ||||
|             imagePath: { | ||||
|               [sequelize.Op.or]: [null, ''] | ||||
|             } | ||||
|           }, | ||||
|           sequelize.where(sequelize.literal('(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)'), 0) | ||||
|         ], | ||||
|         attributes: ['id', 'name'], | ||||
|         raw: true | ||||
|       }) | ||||
|     ).map((au) => ({ id: au.id, name: au.name })) | ||||
| 
 | ||||
|     if (bookAuthorsToRemove.length) { | ||||
|       await Database.authorModel.destroy({ | ||||
|         where: { | ||||
|           id: bookAuthorsToRemove.map((au) => au.id) | ||||
|         } | ||||
|       }) | ||||
|       bookAuthorsToRemove.forEach(({ id, name }) => { | ||||
|         Database.removeAuthorFromFilterData(libraryId, id) | ||||
|         // TODO: Clients were expecting full author in payload but its unnecessary
 | ||||
|         SocketAuthority.emitter('author_removed', { id, libraryId }) | ||||
|         Logger.info(`[ApiRouter] Removed author "${name}" with no books`) | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Remove an empty series & close an open RSS feed | ||||
|    * @param {import('../models/Series')} series | ||||
| @ -567,11 +615,13 @@ class ApiRouter { | ||||
|           } | ||||
| 
 | ||||
|           if (!mediaMetadata.authors[i].id) { | ||||
|             let author = await Database.authorModel.getOldByNameAndLibrary(authorName, libraryId) | ||||
|             let author = await Database.authorModel.getByNameAndLibrary(authorName, libraryId) | ||||
|             if (!author) { | ||||
|               author = new Author() | ||||
|               author.setData(mediaMetadata.authors[i], libraryId) | ||||
|               Logger.debug(`[ApiRouter] Created new author "${author.name}"`) | ||||
|               author = await Database.authorModel.create({ | ||||
|                 name: authorName, | ||||
|                 libraryId | ||||
|               }) | ||||
|               Logger.debug(`[ApiRouter] Creating new author "${author.name}"`) | ||||
|               newAuthors.push(author) | ||||
|               // Update filter data
 | ||||
|               Database.addAuthorToFilterData(libraryId, author.name, author.id) | ||||
| @ -584,10 +634,9 @@ class ApiRouter { | ||||
|         // Remove authors without an id
 | ||||
|         mediaMetadata.authors = mediaMetadata.authors.filter((au) => !!au.id) | ||||
|         if (newAuthors.length) { | ||||
|           await Database.createBulkAuthors(newAuthors) | ||||
|           SocketAuthority.emitter( | ||||
|             'authors_added', | ||||
|             newAuthors.map((au) => au.toJSON()) | ||||
|             newAuthors.map((au) => au.toOldJSON()) | ||||
|           ) | ||||
|         } | ||||
|       } | ||||
|  | ||||
| @ -8,7 +8,6 @@ const { findMatchingEpisodesInFeed, getPodcastFeed } = require('../utils/podcast | ||||
| const BookFinder = require('../finders/BookFinder') | ||||
| const PodcastFinder = require('../finders/PodcastFinder') | ||||
| const LibraryScan = require('./LibraryScan') | ||||
| const Author = require('../objects/entities/Author') | ||||
| const Series = require('../objects/entities/Series') | ||||
| const LibraryScanner = require('./LibraryScanner') | ||||
| const CoverManager = require('../managers/CoverManager') | ||||
| @ -206,12 +205,13 @@ class Scanner { | ||||
|       } | ||||
|       const authorPayload = [] | ||||
|       for (const authorName of matchData.author) { | ||||
|         let author = await Database.authorModel.getOldByNameAndLibrary(authorName, libraryItem.libraryId) | ||||
|         let author = await Database.authorModel.getByNameAndLibrary(authorName, libraryItem.libraryId) | ||||
|         if (!author) { | ||||
|           author = new Author() | ||||
|           author.setData({ name: authorName }, libraryItem.libraryId) | ||||
|           await Database.createAuthor(author) | ||||
|           SocketAuthority.emitter('author_added', author.toJSON()) | ||||
|           author = await Database.authorModel.create({ | ||||
|             name: authorName, | ||||
|             libraryId: libraryItem.libraryId | ||||
|           }) | ||||
|           SocketAuthority.emitter('author_added', author.toOldJSON()) | ||||
|           // Update filter data
 | ||||
|           Database.addAuthorToFilterData(libraryItem.libraryId, author.name, author.id) | ||||
|         } | ||||
|  | ||||
| @ -42,15 +42,15 @@ module.exports.parse = (nameString) => { | ||||
|   var splitNames = [] | ||||
|   // Example &LF: Friedman, Milton & Friedman, Rose
 | ||||
|   if (nameString.includes('&')) { | ||||
|     nameString.split('&').forEach((asa) => splitNames = splitNames.concat(asa.split(','))) | ||||
|     nameString.split('&').forEach((asa) => (splitNames = splitNames.concat(asa.split(',')))) | ||||
|   } else if (nameString.includes(' and ')) { | ||||
|     nameString.split(' and ').forEach((asa) => splitNames = splitNames.concat(asa.split(','))) | ||||
|     nameString.split(' and ').forEach((asa) => (splitNames = splitNames.concat(asa.split(',')))) | ||||
|   } else if (nameString.includes(';')) { | ||||
|     nameString.split(';').forEach((asa) => splitNames = splitNames.concat(asa.split(','))) | ||||
|     nameString.split(';').forEach((asa) => (splitNames = splitNames.concat(asa.split(',')))) | ||||
|   } else { | ||||
|     splitNames = nameString.split(',') | ||||
|   } | ||||
|   if (splitNames.length) splitNames = splitNames.map(a => a.trim()) | ||||
|   if (splitNames.length) splitNames = splitNames.map((a) => a.trim()) | ||||
| 
 | ||||
|   var names = [] | ||||
| 
 | ||||
| @ -84,21 +84,12 @@ module.exports.parse = (nameString) => { | ||||
|   } | ||||
| 
 | ||||
|   // Filter out names that have no first and last
 | ||||
|   names = names.filter(n => n.first_name || n.last_name) | ||||
|   names = names.filter((n) => n.first_name || n.last_name) | ||||
| 
 | ||||
|   // Set name strings and remove duplicates
 | ||||
|   const namesArray = [...new Set(names.map(a => a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name))] | ||||
|   const namesArray = [...new Set(names.map((a) => (a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name)))] | ||||
| 
 | ||||
|   return { | ||||
|     names: namesArray // Array of first last
 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| module.exports.checkNamesAreEqual = (name1, name2) => { | ||||
|   if (!name1 || !name2) return false | ||||
| 
 | ||||
|   // e.g. John H. Smith will be equal to John H Smith
 | ||||
|   name1 = String(name1).toLowerCase().trim().replace(/\./g, '') | ||||
|   name2 = String(name2).toLowerCase().trim().replace(/\./g, '') | ||||
|   return name1 === name2 | ||||
| } | ||||
| @ -73,8 +73,7 @@ module.exports = { | ||||
|     }) | ||||
|     const authorMatches = [] | ||||
|     for (const author of authors) { | ||||
|       const oldAuthor = author.getOldAuthor().toJSON() | ||||
|       oldAuthor.numBooks = author.dataValues.numBooks | ||||
|       const oldAuthor = author.toOldJSONExpanded(author.dataValues.numBooks) | ||||
|       authorMatches.push(oldAuthor) | ||||
|     } | ||||
|     return authorMatches | ||||
|  | ||||
| @ -353,7 +353,7 @@ module.exports = { | ||||
|     return { | ||||
|       authors: authors.map((au) => { | ||||
|         const numBooks = au.books.length || 0 | ||||
|         return au.getOldAuthor().toJSONExpanded(numBooks) | ||||
|         return au.toOldJSONExpanded(numBooks) | ||||
|       }), | ||||
|       count | ||||
|     } | ||||
| @ -409,7 +409,7 @@ module.exports = { | ||||
| 
 | ||||
|   /** | ||||
|    * Get library items for an author, optional use user permissions | ||||
|    * @param {oldAuthor} author | ||||
|    * @param {import('../../models/Author')} author | ||||
|    * @param {import('../../models/User')} user | ||||
|    * @param {number} limit | ||||
|    * @param {number} offset | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user