mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-08-01 16:54:59 +02:00
Remove old Author object & fix issue deleting empty authors
This commit is contained in:
parent
acc4bdbc5e
commit
ba742563c2
13 changed files with 227 additions and 314 deletions
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue