Add initial Author name normalization and calculated column updates

This commit is contained in:
Nicholas Wallace 2026-05-11 19:40:46 -07:00
parent aacdcc47ec
commit bf76128203
2 changed files with 77 additions and 23 deletions

View file

@ -1,4 +1,4 @@
const { DataTypes, Model, where, fn, col } = require('sequelize')
const { DataTypes, Model } = require('sequelize')
const parseNameString = require('../utils/parsers/parseNameString')
class Author extends Model {
@ -12,6 +12,8 @@ class Author extends Model {
/** @type {string} */
this.lastFirst
/** @type {string} */
this.searchName
/** @type {string} */
this.asin
/** @type {string} */
this.description
@ -35,6 +37,35 @@ class Author extends Model {
return parseNameString.nameToLastFirst(name)
}
static normalizeSearchName(name) {
if (!name?.trim()) return null
return name
.normalize('NFKC') // Standardize compatibility characters
.normalize('NFD') // Split accents into combining marks
.toLocaleLowerCase('und')
.replace(/[\p{P}\p{Z}\p{M}\s]+/gu, '') // Remove punctuation, whitespace, and diacritics
.trim()
}
static buildAuthorDerivedFields(name) {
const searchName = this.normalizeSearchName(name)
if (!searchName) {
return {
lastFirst: null,
searchName: null
}
}
return {
lastFirst: parseNameString.nameToLastFirst(name),
searchName
}
}
static isAuthorNameMatch(leftName, rightName) {
return this.normalizeSearchName(leftName) === this.normalizeSearchName(rightName)
}
/**
* Check if author exists
* @param {string} authorId
@ -53,13 +84,13 @@ class Author extends Model {
* @returns {Promise<Author>}
*/
static async getByNameAndLibrary(authorName, libraryId) {
const searchName = this.normalizeSearchName(authorName)
if (!searchName) return null
return this.findOne({
where: [
where(fn('lower', col('name')), authorName.toLowerCase()),
{
libraryId
}
]
where: {
searchName,
libraryId
}
})
}
@ -114,14 +145,23 @@ class Author extends Model {
* @returns {Promise<{ author: Author, created: boolean }>}
*/
static async findOrCreateByNameAndLibrary(name, libraryId) {
const author = await this.getByNameAndLibrary(name, libraryId)
if (author) return { author, created: false }
const newAuthor = await this.create({
name,
lastFirst: this.getLastFirst(name),
libraryId
const searchName = this.normalizeSearchName(name)
if (!searchName) {
return { author: null, created: false }
}
const [author, created] = await this.findOrCreate({
where: {
searchName,
libraryId
},
defaults: {
name,
libraryId,
...this.buildAuthorDerivedFields(name)
}
})
return { author: newAuthor, created: true }
return { author, created }
}
/**
@ -138,6 +178,7 @@ class Author extends Model {
},
name: DataTypes.STRING,
lastFirst: DataTypes.STRING,
searchName: DataTypes.STRING,
asin: DataTypes.STRING,
description: DataTypes.TEXT,
imagePath: DataTypes.STRING
@ -160,6 +201,19 @@ class Author extends Model {
// collate: 'NOCASE'
// }]
// },
{
fields: [
{
name: 'searchName',
collate: 'NOCASE'
}
]
},
{
fields: ['searchName', 'libraryId'],
unique: true,
name: 'unique_author_search_name_per_library'
},
{
fields: ['libraryId']
}
@ -167,6 +221,10 @@ class Author extends Model {
}
)
Author.beforeSave((author) => {
Object.assign(author, Author.buildAuthorDerivedFields(author.name))
})
const { library } = sequelize.models
library.hasMany(Author, {
onDelete: 'CASCADE'

View file

@ -247,15 +247,11 @@ class Scanner {
}
const authorIdsRemoved = []
for (const authorName of matchData.author) {
const existingAuthor = libraryItem.media.authors.find((a) => a.name.toLowerCase() === authorName.toLowerCase())
const existingAuthor = libraryItem.media.authors.find((a) => Database.authorModel.isAuthorNameMatch(a.name, authorName))
if (!existingAuthor) {
let author = await Database.authorModel.getByNameAndLibrary(authorName, libraryItem.libraryId)
if (!author) {
author = await Database.authorModel.create({
name: authorName,
lastFirst: Database.authorModel.getLastFirst(authorName),
libraryId: libraryItem.libraryId
})
const { author, created: isCreated } = await Database.authorModel.findOrCreateByNameAndLibrary(authorName, libraryItem.libraryId)
if (!author) continue
if (isCreated) {
SocketAuthority.emitter('author_added', author.toOldJSON())
// Update filter data
Database.addAuthorToFilterData(libraryItem.libraryId, author.name, author.id)
@ -271,7 +267,7 @@ class Scanner {
hasAuthorUpdates = true
})
}
const authorsRemoved = libraryItem.media.authors.filter((a) => !matchData.author.find((ma) => ma.toLowerCase() === a.name.toLowerCase()))
const authorsRemoved = libraryItem.media.authors.filter((a) => !matchData.author.find((ma) => Database.authorModel.isAuthorNameMatch(ma, a.name)))
if (authorsRemoved.length) {
for (const author of authorsRemoved) {
await Database.bookAuthorModel.destroy({ where: { authorId: author.id, bookId: libraryItem.media.id } })