Remove old Author object & fix issue deleting empty authors

This commit is contained in:
advplyr 2024-08-31 13:27:48 -05:00
parent acc4bdbc5e
commit ba742563c2
13 changed files with 227 additions and 314 deletions

View file

@ -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) {

View file

@ -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)
}

View file

@ -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)
}