diff --git a/server/providers/iTunes.js b/server/providers/iTunes.js index 87695c58c..6d14471b9 100644 --- a/server/providers/iTunes.js +++ b/server/providers/iTunes.js @@ -27,6 +27,55 @@ const htmlSanitizer = require('../utils/htmlSanitizer') * @property {boolean} explicit */ +/** + * Split a list of authors on commas. A piece with no space is a credential and is glued onto the previous name. + * + * @param {string} text + * @returns {string[]} + */ +function parseAuthorPieces(text) { + const authors = [] + for (const piece of (text || '').split(',')) { + const name = piece.trim() + if (!name) continue + if (!name.includes(' ') && authors.length) { + authors[authors.length - 1] = `${authors[authors.length - 1]} ${name}` + } else { + authors.push(name) + } + } + return authors +} + +function stripCommasFromName(name) { + return (name || '').replace(/,/g, '').replace(/\s+/g, ' ').trim() +} + +/** + * Apple lists authors as "Name1, Name2 & Name3", but a comma can also sit inside one name ("Rick Hanson, PhD"). + * A comma-separated piece with no space is treated as a credential and glued onto the previous name. + * The text after the last " & " is a single author, so commas there are removed. + * @see https://github.com/advplyr/audiobookshelf/issues/1022 + * @see https://github.com/advplyr/audiobookshelf/issues/2661 + * + * @param {string} artistName + * @returns {string} + */ +function cleanAuthor(artistName) { + const raw = (artistName || '').trim() + if (!raw) return '' + + const lastAmpIndex = raw.lastIndexOf(' & ') + if (lastAmpIndex === -1) { + return stripCommasFromName(raw) + } + + const authors = parseAuthorPieces(raw.slice(0, lastAmpIndex)) + const lastAuthor = stripCommasFromName(raw.slice(lastAmpIndex + 3)) + if (lastAuthor) authors.push(lastAuthor) + return authors.join(', ') +} + class iTunes { #responseTimeout = 10000 @@ -103,15 +152,11 @@ class iTunes { } cleanAudiobook(data) { - // artistName can be "Name1, Name2 & Name3" so we refactor this to "Name1, Name2, Name3" - // see: https://github.com/advplyr/audiobookshelf/issues/1022 - const author = (data.artistName || '').split(' & ').join(', ') - return { id: data.collectionId, artistId: data.artistId, title: data.collectionName, - author, + author: cleanAuthor(data.artistName), description: data.description || null, publishedYear: data.releaseDate ? data.releaseDate.split('-')[0] : null, genres: data.primaryGenreName ? [data.primaryGenreName] : null, diff --git a/test/server/providers/iTunes.test.js b/test/server/providers/iTunes.test.js new file mode 100644 index 000000000..14f71bbd9 --- /dev/null +++ b/test/server/providers/iTunes.test.js @@ -0,0 +1,53 @@ +const iTunes = require('../../../server/providers/iTunes') +const { expect } = require('chai') + +describe('iTunes', () => { + let itunes + + beforeEach(() => { + itunes = new iTunes() + }) + + describe('cleanAudiobook', () => { + function authorFrom(artistName) { + return itunes.cleanAudiobook({ artistName }).author + } + + it('should return an empty string if artistName is missing', () => { + expect(authorFrom()).to.equal('') + expect(authorFrom('')).to.equal('') + }) + + it('should keep a single author without commas', () => { + expect(authorFrom('Joy Fielding')).to.equal('Joy Fielding') + }) + + it('should strip commas from a single author with a credential', () => { + expect(authorFrom('Rick Hanson, PhD')).to.equal('Rick Hanson PhD') + }) + + it('should split two authors joined by an ampersand', () => { + expect(authorFrom('Name1 & Name2')).to.equal('Name1, Name2') + }) + + it('should split three authors using commas and an ampersand', () => { + expect(authorFrom('Keltie Knight, Becca Tobin & Jac Vanek')).to.equal('Keltie Knight, Becca Tobin, Jac Vanek') + }) + + it('should glue a one-word credential onto the previous author before the ampersand', () => { + expect(authorFrom('Rick Hanson, PhD & Jane Doe')).to.equal('Rick Hanson PhD, Jane Doe') + }) + + it('should glue several one-word credentials in a row', () => { + expect(authorFrom('Rick Hanson, PhD, MD & Jane Doe')).to.equal('Rick Hanson PhD MD, Jane Doe') + }) + + it('should strip commas from the last author', () => { + expect(authorFrom('Rick Hanson & Jane Doe, PhD')).to.equal('Rick Hanson, Jane Doe PhD') + }) + + it('should glue credentials before the ampersand and strip commas from the last author', () => { + expect(authorFrom('Rick Hanson, PhD & Jane Doe, PhD')).to.equal('Rick Hanson PhD, Jane Doe PhD') + }) + }) +})