mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-11-07 08:34:10 +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