From f1f57d6595f16eebaae2099065f8a0c9e7a45e4e Mon Sep 17 00:00:00 2001 From: mikiher Date: Sun, 1 Nov 2026 09:10:45 +0200 Subject: [PATCH] Add one name dedupe function and use it for parsed names and metadata.json --- .../utils/generators/abmetadataGenerator.js | 4 +-- server/utils/parsers/parseNameString.js | 26 +++++++++++++++++-- .../utils/parsers/parseNameString.test.js | 16 +++++++++++- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/server/utils/generators/abmetadataGenerator.js b/server/utils/generators/abmetadataGenerator.js index d0d278752..0144cada3 100644 --- a/server/utils/generators/abmetadataGenerator.js +++ b/server/utils/generators/abmetadataGenerator.js @@ -1,5 +1,6 @@ const Logger = require('../../Logger') const parseSeriesString = require('../parsers/parseSeriesString') +const { dedupeNames } = require('../parsers/parseNameString') const mediaTypeKeys = { book: { @@ -140,8 +141,7 @@ function validateMetadataValue(key, value, expectedType) { return undefined } - const cleanedArray = value.filter((t) => typeof t === 'string') - return [...new Set(cleanedArray.map((t) => t.trim()).filter((t) => t))] + return dedupeNames(value) } Logger.warn(`[abmetadataGenerator] Unknown expected type "${expectedType}" for key "${key}"`) diff --git a/server/utils/parsers/parseNameString.js b/server/utils/parsers/parseNameString.js index 096455cd4..39296d00e 100644 --- a/server/utils/parsers/parseNameString.js +++ b/server/utils/parsers/parseNameString.js @@ -35,6 +35,28 @@ module.exports.nameToLastFirst = (firstLast) => { return `${nameObj.last_name}, ${nameObj.first_name}` } +/** + * Trim names, drop blanks, and drop exact repeats. The first spelling is kept. + * Callers that build bookAuthors rows should use this instead of deduping per metadata source. + * + * @param {string[]} names + * @returns {string[]} + */ +function dedupeNames(names) { + if (!Array.isArray(names)) return [] + const seen = new Set() + const deduped = [] + for (const name of names) { + if (typeof name !== 'string') continue + const trimmed = name.trim() + if (!trimmed || seen.has(trimmed)) continue + seen.add(trimmed) + deduped.push(trimmed) + } + return deduped +} +module.exports.dedupeNames = dedupeNames + /** * Parses a name string into an array of names * @@ -62,7 +84,7 @@ module.exports.parse = (nameString) => { // If names are in Chinese,Japanese and Korean languages, return as is. if (/[\u4e00-\u9fff\u3040-\u30ff\u31f0-\u31ff]/.test(splitNames[0])) { return { - names: splitNames + names: dedupeNames(splitNames) } } @@ -102,7 +124,7 @@ module.exports.parse = (nameString) => { 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 = dedupeNames(names.map((a) => (a.first_name ? `${a.first_name} ${a.last_name}` : a.last_name))) return { names: namesArray // Array of first last diff --git a/test/server/utils/parsers/parseNameString.test.js b/test/server/utils/parsers/parseNameString.test.js index 4d2418925..529721f0c 100644 --- a/test/server/utils/parsers/parseNameString.test.js +++ b/test/server/utils/parsers/parseNameString.test.js @@ -1,6 +1,6 @@ const chai = require('chai') const expect = chai.expect -const { parse, nameToLastFirst } = require('../../../../server/utils/parsers/parseNameString') +const { parse, nameToLastFirst, dedupeNames } = require('../../../../server/utils/parsers/parseNameString') describe('parseNameString', () => { describe('parse', () => { @@ -80,6 +80,20 @@ describe('parseNameString', () => { }) }) + describe('dedupeNames', () => { + it('trims, drops blanks, and keeps the first spelling of an exact repeat', () => { + expect(dedupeNames([' Stephen King ', 'Stephen King', '', ' ', 'stephen king'])).to.deep.equal(['Stephen King', 'stephen king']) + }) + + it('preserves order and ignores non-strings', () => { + expect(dedupeNames(['Amy Yao', null, 'Amy Yao', 3, 'Raymond Chandler'])).to.deep.equal(['Amy Yao', 'Raymond Chandler']) + }) + + it('returns an empty array for non-arrays', () => { + expect(dedupeNames(null)).to.deep.equal([]) + }) + }) + describe('nameToLastFirst', () => { it('converts First Last to Last, First format', () => { const result = nameToLastFirst('John Smith')