mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-24 22:47:09 -04: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
|
@ -462,26 +462,6 @@ class Database {
|
||||||
await this.models.series.removeById(seriesId)
|
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) {
|
async createBulkBookAuthors(bookAuthors) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
await this.models.bookAuthor.bulkCreate(bookAuthors)
|
await this.models.bookAuthor.bulkCreate(bookAuthors)
|
||||||
|
@ -684,7 +664,7 @@ class Database {
|
||||||
*/
|
*/
|
||||||
async getAuthorIdByName(libraryId, authorName) {
|
async getAuthorIdByName(libraryId, authorName) {
|
||||||
if (!this.libraryFilterData[libraryId]) {
|
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
|
return this.libraryFilterData[libraryId].authors.find((au) => au.name === authorName)?.id || null
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,11 @@ const naturalSort = createNewSortInstance({
|
||||||
* @property {import('../models/User')} user
|
* @property {import('../models/User')} user
|
||||||
*
|
*
|
||||||
* @typedef {Request & RequestUserObject} RequestWithUser
|
* @typedef {Request & RequestUserObject} RequestWithUser
|
||||||
|
*
|
||||||
|
* @typedef RequestEntityObject
|
||||||
|
* @property {import('../models/Author')} author
|
||||||
|
*
|
||||||
|
* @typedef {RequestWithUser & RequestEntityObject} AuthorControllerRequest
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class AuthorController {
|
class AuthorController {
|
||||||
|
@ -29,13 +34,13 @@ class AuthorController {
|
||||||
/**
|
/**
|
||||||
* GET: /api/authors/:id
|
* GET: /api/authors/:id
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {AuthorControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
const include = (req.query.include || '').split(',')
|
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
|
// Used on author landing page to include library items and items grouped in series
|
||||||
if (include.includes('items')) {
|
if (include.includes('items')) {
|
||||||
|
@ -80,25 +85,30 @@ class AuthorController {
|
||||||
/**
|
/**
|
||||||
* PATCH: /api/authors/:id
|
* PATCH: /api/authors/:id
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {AuthorControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
const payload = req.body
|
const keysToUpdate = ['name', 'description', 'asin']
|
||||||
let hasUpdated = false
|
const payload = {}
|
||||||
|
for (const key in req.body) {
|
||||||
// author imagePath must be set through other endpoints as of v2.4.5
|
if (keysToUpdate.includes(key) && (typeof req.body[key] === 'string' || req.body[key] === null)) {
|
||||||
if (payload.imagePath !== undefined) {
|
payload[key] = req.body[key]
|
||||||
Logger.warn(`[AuthorController] Updating local author imagePath is not supported`)
|
}
|
||||||
delete payload.imagePath
|
|
||||||
}
|
}
|
||||||
|
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
|
const authorNameUpdate = payload.name !== undefined && payload.name !== req.author.name
|
||||||
|
|
||||||
// Check if author name matches another author and merge the authors
|
// Check if author name matches another author and merge the authors
|
||||||
let existingAuthor = null
|
let existingAuthor = null
|
||||||
if (authorNameUpdate) {
|
if (authorNameUpdate) {
|
||||||
const author = await Database.authorModel.findOne({
|
existingAuthor = await Database.authorModel.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: {
|
id: {
|
||||||
[sequelize.Op.not]: req.author.id
|
[sequelize.Op.not]: req.author.id
|
||||||
|
@ -106,7 +116,6 @@ class AuthorController {
|
||||||
name: payload.name
|
name: payload.name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
existingAuthor = author?.getOldAuthor()
|
|
||||||
}
|
}
|
||||||
if (existingAuthor) {
|
if (existingAuthor) {
|
||||||
Logger.info(`[AuthorController] Merging author "${req.author.name}" with "${existingAuthor.name}"`)
|
Logger.info(`[AuthorController] Merging author "${req.author.name}" with "${existingAuthor.name}"`)
|
||||||
|
@ -143,86 +152,87 @@ class AuthorController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove old author
|
// Remove old author
|
||||||
await Database.removeAuthor(req.author.id)
|
const oldAuthorJSON = req.author.toOldJSON()
|
||||||
SocketAuthority.emitter('author_removed', req.author.toJSON())
|
await req.author.destroy()
|
||||||
|
SocketAuthority.emitter('author_removed', oldAuthorJSON)
|
||||||
// Update filter data
|
// Update filter data
|
||||||
Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
|
Database.removeAuthorFromFilterData(oldAuthorJSON.libraryId, oldAuthorJSON.id)
|
||||||
|
|
||||||
// Send updated num books for merged author
|
// Send updated num books for merged author
|
||||||
const numBooks = await Database.bookAuthorModel.getCountForAuthor(existingAuthor.id)
|
const numBooks = await Database.bookAuthorModel.getCountForAuthor(existingAuthor.id)
|
||||||
SocketAuthority.emitter('author_updated', existingAuthor.toJSONExpanded(numBooks))
|
SocketAuthority.emitter('author_updated', existingAuthor.toOldJSONExpanded(numBooks))
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
author: existingAuthor.toJSON(),
|
author: existingAuthor.toOldJSON(),
|
||||||
merged: true
|
merged: true
|
||||||
})
|
})
|
||||||
} else {
|
return
|
||||||
// Regular author update
|
}
|
||||||
if (req.author.update(payload)) {
|
|
||||||
hasUpdated = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasUpdated) {
|
// Regular author update
|
||||||
req.author.updatedAt = Date.now()
|
req.author.set(payload)
|
||||||
|
if (req.author.changed()) {
|
||||||
|
await req.author.save()
|
||||||
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
|
||||||
let numBooksForAuthor = 0
|
if (hasUpdated) {
|
||||||
if (authorNameUpdate) {
|
let numBooksForAuthor = 0
|
||||||
const allItemsWithAuthor = await Database.authorModel.getAllLibraryItemsForAuthor(req.author.id)
|
if (authorNameUpdate) {
|
||||||
|
const allItemsWithAuthor = await Database.authorModel.getAllLibraryItemsForAuthor(req.author.id)
|
||||||
|
|
||||||
numBooksForAuthor = allItemsWithAuthor.length
|
numBooksForAuthor = allItemsWithAuthor.length
|
||||||
const oldLibraryItems = []
|
const oldLibraryItems = []
|
||||||
// Update author name on all books
|
// Update author name on all books
|
||||||
for (const libraryItem of allItemsWithAuthor) {
|
for (const libraryItem of allItemsWithAuthor) {
|
||||||
libraryItem.media.authors = libraryItem.media.authors.map((au) => {
|
libraryItem.media.authors = libraryItem.media.authors.map((au) => {
|
||||||
if (au.id === req.author.id) {
|
if (au.id === req.author.id) {
|
||||||
au.name = req.author.name
|
au.name = req.author.name
|
||||||
}
|
}
|
||||||
return au
|
return au
|
||||||
})
|
})
|
||||||
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
|
||||||
oldLibraryItems.push(oldLibraryItem)
|
oldLibraryItems.push(oldLibraryItem)
|
||||||
|
|
||||||
await libraryItem.saveMetadataFile()
|
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 Database.updateAuthor(req.author)
|
if (oldLibraryItems.length) {
|
||||||
SocketAuthority.emitter('author_updated', req.author.toJSONExpanded(numBooksForAuthor))
|
SocketAuthority.emitter(
|
||||||
|
'items_updated',
|
||||||
|
oldLibraryItems.map((li) => li.toJSONExpanded())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
numBooksForAuthor = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
SocketAuthority.emitter('author_updated', req.author.toOldJSONExpanded(numBooksForAuthor))
|
||||||
author: req.author.toJSON(),
|
|
||||||
updated: hasUpdated
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
author: req.author.toOldJSON(),
|
||||||
|
updated: hasUpdated
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DELETE: /api/authors/:id
|
* DELETE: /api/authors/:id
|
||||||
* Remove author from all books and delete
|
* Remove author from all books and delete
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {AuthorControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
Logger.info(`[AuthorController] Removing author "${req.author.name}"`)
|
Logger.info(`[AuthorController] Removing author "${req.author.name}"`)
|
||||||
|
|
||||||
await Database.authorModel.removeById(req.author.id)
|
|
||||||
|
|
||||||
if (req.author.imagePath) {
|
if (req.author.imagePath) {
|
||||||
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
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
|
// Update filter data
|
||||||
Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
|
Database.removeAuthorFromFilterData(req.author.libraryId, req.author.id)
|
||||||
|
@ -234,7 +244,7 @@ class AuthorController {
|
||||||
* POST: /api/authors/:id/image
|
* POST: /api/authors/:id/image
|
||||||
* Upload author image from web URL
|
* Upload author image from web URL
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {AuthorControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async uploadImage(req, res) {
|
async uploadImage(req, res) {
|
||||||
|
@ -265,13 +275,14 @@ class AuthorController {
|
||||||
}
|
}
|
||||||
|
|
||||||
req.author.imagePath = result.path
|
req.author.imagePath = result.path
|
||||||
req.author.updatedAt = Date.now()
|
// imagePath may not have changed, but we still want to update the updatedAt field to bust image cache
|
||||||
await Database.authorModel.updateFromOld(req.author)
|
req.author.changed('imagePath', true)
|
||||||
|
await req.author.save()
|
||||||
|
|
||||||
const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
|
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({
|
res.json({
|
||||||
author: req.author.toJSON()
|
author: req.author.toOldJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,7 +290,7 @@ class AuthorController {
|
||||||
* DELETE: /api/authors/:id/image
|
* DELETE: /api/authors/:id/image
|
||||||
* Remove author image & delete image file
|
* Remove author image & delete image file
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {AuthorControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async deleteImage(req, res) {
|
async deleteImage(req, res) {
|
||||||
|
@ -291,19 +302,19 @@ class AuthorController {
|
||||||
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
await CacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||||
await CoverManager.removeFile(req.author.imagePath)
|
await CoverManager.removeFile(req.author.imagePath)
|
||||||
req.author.imagePath = null
|
req.author.imagePath = null
|
||||||
await Database.authorModel.updateFromOld(req.author)
|
await req.author.save()
|
||||||
|
|
||||||
const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
|
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({
|
res.json({
|
||||||
author: req.author.toJSON()
|
author: req.author.toOldJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST: /api/authors/:id/match
|
* POST: /api/authors/:id/match
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {AuthorControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async match(req, res) {
|
async match(req, res) {
|
||||||
|
@ -342,24 +353,22 @@ class AuthorController {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
req.author.updatedAt = Date.now()
|
await req.author.save()
|
||||||
|
|
||||||
await Database.updateAuthor(req.author)
|
|
||||||
|
|
||||||
const numBooks = await Database.bookAuthorModel.getCountForAuthor(req.author.id)
|
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({
|
res.json({
|
||||||
updated: hasUpdates,
|
updated: hasUpdates,
|
||||||
author: req.author
|
author: req.author.toOldJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET: /api/authors/:id/image
|
* GET: /api/authors/:id/image
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {AuthorControllerRequest} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async getImage(req, res) {
|
async getImage(req, res) {
|
||||||
|
@ -392,7 +401,7 @@ class AuthorController {
|
||||||
* @param {NextFunction} next
|
* @param {NextFunction} next
|
||||||
*/
|
*/
|
||||||
async middleware(req, res, 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 (!author) return res.sendStatus(404)
|
||||||
|
|
||||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
|
|
|
@ -887,8 +887,7 @@ class LibraryController {
|
||||||
const oldAuthors = []
|
const oldAuthors = []
|
||||||
|
|
||||||
for (const author of authors) {
|
for (const author of authors) {
|
||||||
const oldAuthor = author.getOldAuthor().toJSON()
|
const oldAuthor = author.toOldJSONExpanded(author.books.length)
|
||||||
oldAuthor.numBooks = author.books.length
|
|
||||||
oldAuthor.lastFirst = author.lastFirst
|
oldAuthor.lastFirst = author.lastFirst
|
||||||
oldAuthors.push(oldAuthor)
|
oldAuthors.push(oldAuthor)
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,6 +151,8 @@ class LibraryItemController {
|
||||||
* PATCH: /items/:id/media
|
* PATCH: /items/:id/media
|
||||||
* Update media for a library item. Will create new authors & series when necessary
|
* Update media for a library item. Will create new authors & series when necessary
|
||||||
*
|
*
|
||||||
|
* @this {import('../routers/ApiRouter')}
|
||||||
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {RequestWithUser} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
|
@ -185,6 +187,12 @@ class LibraryItemController {
|
||||||
seriesRemoved = libraryItem.media.metadata.series.filter((se) => !seriesIdsInUpdate.includes(se.id))
|
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
|
const hasUpdates = libraryItem.media.update(mediaPayload) || mediaPayload.url
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
libraryItem.updatedAt = Date.now()
|
libraryItem.updatedAt = Date.now()
|
||||||
|
@ -205,6 +213,15 @@ class LibraryItemController {
|
||||||
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
Logger.debug(`[LibraryItemController] Updated library item media ${libraryItem.media.metadata.title}`)
|
||||||
await Database.updateLibraryItem(libraryItem)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
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({
|
res.json({
|
||||||
updated: hasUpdates,
|
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.
|
// 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'
|
const isAppleMobileBrowser = ua.device.vendor === 'Apple' && ua.device.type === 'mobile' && ua.engine.name === 'WebKit'
|
||||||
if (isAppleMobileBrowser && audioMimeType === AudioMimeType.M4B) {
|
if (isAppleMobileBrowser && audioMimeType === AudioMimeType.M4B) {
|
||||||
audioMimeType = 'audio/m4b'
|
audioMimeType = 'audio/m4b'
|
||||||
}
|
}
|
||||||
res.setHeader('Content-Type', audioMimeType)
|
res.setHeader('Content-Type', audioMimeType)
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,6 +124,13 @@ class CacheManager {
|
||||||
await this.ensureCachePaths()
|
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 = {}) {
|
async handleAuthorCache(res, author, options = {}) {
|
||||||
const format = options.format || 'webp'
|
const format = options.format || 'webp'
|
||||||
const width = options.width || 400
|
const width = options.width || 400
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
const { DataTypes, Model, where, fn, col } = require('sequelize')
|
const { DataTypes, Model, where, fn, col } = require('sequelize')
|
||||||
|
|
||||||
const oldAuthor = require('../objects/entities/Author')
|
|
||||||
|
|
||||||
class Author extends Model {
|
class Author extends Model {
|
||||||
constructor(values, options) {
|
constructor(values, options) {
|
||||||
super(values, options)
|
super(values, options)
|
||||||
|
@ -26,69 +24,6 @@ class Author extends Model {
|
||||||
this.createdAt
|
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
|
* Check if author exists
|
||||||
* @param {string} authorId
|
* @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
|
* TODO: Look for authors ignoring punctuation
|
||||||
*
|
*
|
||||||
* @param {string} authorName
|
* @param {string} authorName
|
||||||
* @param {string} libraryId
|
* @param {string} libraryId
|
||||||
* @returns {Promise<oldAuthor>}
|
* @returns {Promise<Author>}
|
||||||
*/
|
*/
|
||||||
static async getOldByNameAndLibrary(authorName, libraryId) {
|
static async getByNameAndLibrary(authorName, libraryId) {
|
||||||
const author = (
|
return this.findOne({
|
||||||
await this.findOne({
|
where: [
|
||||||
where: [
|
where(fn('lower', col('name')), authorName.toLowerCase()),
|
||||||
where(fn('lower', col('name')), authorName.toLowerCase()),
|
{
|
||||||
{
|
libraryId
|
||||||
libraryId
|
}
|
||||||
}
|
]
|
||||||
]
|
})
|
||||||
})
|
|
||||||
)?.getOldAuthor()
|
|
||||||
return author
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -213,5 +145,36 @@ class Author extends Model {
|
||||||
})
|
})
|
||||||
Author.belongsTo(library)
|
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
|
module.exports = Author
|
||||||
|
|
|
@ -773,7 +773,7 @@ class LibraryItem extends Model {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get book library items for author, optional use user permissions
|
* Get book library items for author, optional use user permissions
|
||||||
* @param {oldAuthor} author
|
* @param {import('./Author')} author
|
||||||
* @param {import('./User')} user
|
* @param {import('./User')} user
|
||||||
* @returns {Promise<oldLibraryItem[]>}
|
* @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 express = require('express')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
const sequelize = require('sequelize')
|
||||||
|
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
|
@ -32,7 +33,6 @@ const CustomMetadataProviderController = require('../controllers/CustomMetadataP
|
||||||
const MiscController = require('../controllers/MiscController')
|
const MiscController = require('../controllers/MiscController')
|
||||||
const ShareController = require('../controllers/ShareController')
|
const ShareController = require('../controllers/ShareController')
|
||||||
|
|
||||||
const Author = require('../objects/entities/Author')
|
|
||||||
const Series = require('../objects/entities/Series')
|
const Series = require('../objects/entities/Series')
|
||||||
|
|
||||||
class ApiRouter {
|
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
|
* Remove an empty series & close an open RSS feed
|
||||||
* @param {import('../models/Series')} series
|
* @param {import('../models/Series')} series
|
||||||
|
@ -567,11 +615,13 @@ class ApiRouter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mediaMetadata.authors[i].id) {
|
if (!mediaMetadata.authors[i].id) {
|
||||||
let author = await Database.authorModel.getOldByNameAndLibrary(authorName, libraryId)
|
let author = await Database.authorModel.getByNameAndLibrary(authorName, libraryId)
|
||||||
if (!author) {
|
if (!author) {
|
||||||
author = new Author()
|
author = await Database.authorModel.create({
|
||||||
author.setData(mediaMetadata.authors[i], libraryId)
|
name: authorName,
|
||||||
Logger.debug(`[ApiRouter] Created new author "${author.name}"`)
|
libraryId
|
||||||
|
})
|
||||||
|
Logger.debug(`[ApiRouter] Creating new author "${author.name}"`)
|
||||||
newAuthors.push(author)
|
newAuthors.push(author)
|
||||||
// Update filter data
|
// Update filter data
|
||||||
Database.addAuthorToFilterData(libraryId, author.name, author.id)
|
Database.addAuthorToFilterData(libraryId, author.name, author.id)
|
||||||
|
@ -584,10 +634,9 @@ class ApiRouter {
|
||||||
// Remove authors without an id
|
// Remove authors without an id
|
||||||
mediaMetadata.authors = mediaMetadata.authors.filter((au) => !!au.id)
|
mediaMetadata.authors = mediaMetadata.authors.filter((au) => !!au.id)
|
||||||
if (newAuthors.length) {
|
if (newAuthors.length) {
|
||||||
await Database.createBulkAuthors(newAuthors)
|
|
||||||
SocketAuthority.emitter(
|
SocketAuthority.emitter(
|
||||||
'authors_added',
|
'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 BookFinder = require('../finders/BookFinder')
|
||||||
const PodcastFinder = require('../finders/PodcastFinder')
|
const PodcastFinder = require('../finders/PodcastFinder')
|
||||||
const LibraryScan = require('./LibraryScan')
|
const LibraryScan = require('./LibraryScan')
|
||||||
const Author = require('../objects/entities/Author')
|
|
||||||
const Series = require('../objects/entities/Series')
|
const Series = require('../objects/entities/Series')
|
||||||
const LibraryScanner = require('./LibraryScanner')
|
const LibraryScanner = require('./LibraryScanner')
|
||||||
const CoverManager = require('../managers/CoverManager')
|
const CoverManager = require('../managers/CoverManager')
|
||||||
|
@ -206,12 +205,13 @@ class Scanner {
|
||||||
}
|
}
|
||||||
const authorPayload = []
|
const authorPayload = []
|
||||||
for (const authorName of matchData.author) {
|
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) {
|
if (!author) {
|
||||||
author = new Author()
|
author = await Database.authorModel.create({
|
||||||
author.setData({ name: authorName }, libraryItem.libraryId)
|
name: authorName,
|
||||||
await Database.createAuthor(author)
|
libraryId: libraryItem.libraryId
|
||||||
SocketAuthority.emitter('author_added', author.toJSON())
|
})
|
||||||
|
SocketAuthority.emitter('author_added', author.toOldJSON())
|
||||||
// Update filter data
|
// Update filter data
|
||||||
Database.addAuthorToFilterData(libraryItem.libraryId, author.name, author.id)
|
Database.addAuthorToFilterData(libraryItem.libraryId, author.name, author.id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,15 +42,15 @@ module.exports.parse = (nameString) => {
|
||||||
var splitNames = []
|
var splitNames = []
|
||||||
// Example &LF: Friedman, Milton & Friedman, Rose
|
// Example &LF: Friedman, Milton & Friedman, Rose
|
||||||
if (nameString.includes('&')) {
|
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 ')) {
|
} 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(';')) {
|
} else if (nameString.includes(';')) {
|
||||||
nameString.split(';').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
|
nameString.split(';').forEach((asa) => (splitNames = splitNames.concat(asa.split(','))))
|
||||||
} else {
|
} else {
|
||||||
splitNames = nameString.split(',')
|
splitNames = nameString.split(',')
|
||||||
}
|
}
|
||||||
if (splitNames.length) splitNames = splitNames.map(a => a.trim())
|
if (splitNames.length) splitNames = splitNames.map((a) => a.trim())
|
||||||
|
|
||||||
var names = []
|
var names = []
|
||||||
|
|
||||||
|
@ -84,21 +84,12 @@ module.exports.parse = (nameString) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out names that have no first and last
|
// 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
|
// 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 {
|
return {
|
||||||
names: namesArray // Array of first last
|
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 = []
|
const authorMatches = []
|
||||||
for (const author of authors) {
|
for (const author of authors) {
|
||||||
const oldAuthor = author.getOldAuthor().toJSON()
|
const oldAuthor = author.toOldJSONExpanded(author.dataValues.numBooks)
|
||||||
oldAuthor.numBooks = author.dataValues.numBooks
|
|
||||||
authorMatches.push(oldAuthor)
|
authorMatches.push(oldAuthor)
|
||||||
}
|
}
|
||||||
return authorMatches
|
return authorMatches
|
||||||
|
|
|
@ -353,7 +353,7 @@ module.exports = {
|
||||||
return {
|
return {
|
||||||
authors: authors.map((au) => {
|
authors: authors.map((au) => {
|
||||||
const numBooks = au.books.length || 0
|
const numBooks = au.books.length || 0
|
||||||
return au.getOldAuthor().toJSONExpanded(numBooks)
|
return au.toOldJSONExpanded(numBooks)
|
||||||
}),
|
}),
|
||||||
count
|
count
|
||||||
}
|
}
|
||||||
|
@ -409,7 +409,7 @@ module.exports = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get library items for an author, optional use user permissions
|
* Get library items for an author, optional use user permissions
|
||||||
* @param {oldAuthor} author
|
* @param {import('../../models/Author')} author
|
||||||
* @param {import('../../models/User')} user
|
* @param {import('../../models/User')} user
|
||||||
* @param {number} limit
|
* @param {number} limit
|
||||||
* @param {number} offset
|
* @param {number} offset
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue