diff --git a/client/components/widgets/BookDetailsEdit.vue b/client/components/widgets/BookDetailsEdit.vue index db3b86ed6..f9ae2d92f 100644 --- a/client/components/widgets/BookDetailsEdit.vue +++ b/client/components/widgets/BookDetailsEdit.vue @@ -16,7 +16,7 @@
- +
diff --git a/server/models/Book.js b/server/models/Book.js index 7e1a2e30c..e3890480b 100644 --- a/server/models/Book.js +++ b/server/models/Book.js @@ -1,6 +1,7 @@ const { DataTypes, Model } = require('sequelize') const Logger = require('../Logger') const { getTitlePrefixAtEnd, getTitleIgnorePrefix } = require('../utils') +const { normalizePublishedYear } = require('../utils/metadataUtils') const parseNameString = require('../utils/parsers/parseNameString') const htmlSanitizer = require('../utils/htmlSanitizer') const libraryItemsBookFilters = require('../utils/queries/libraryItemsBookFilters') @@ -379,6 +380,10 @@ class Book extends Model { payload.metadata[key] = String(payload.metadata[key]) } + if (key === 'publishedYear') { + payload.metadata[key] = normalizePublishedYear(payload.metadata[key]) + } + if ((typeof payload.metadata[key] === 'string' || payload.metadata[key] === null) && this[key] !== payload.metadata[key]) { // Sanitize description HTML if (key === 'description' && payload.metadata[key]) { diff --git a/server/scanner/BookScanner.js b/server/scanner/BookScanner.js index 2b106cdc8..ede0dfd86 100644 --- a/server/scanner/BookScanner.js +++ b/server/scanner/BookScanner.js @@ -3,6 +3,7 @@ const Path = require('path') const sequelize = require('sequelize') const { LogLevel } = require('../utils/constants') const { getTitleIgnorePrefix, areEquivalent } = require('../utils/index') +const { normalizePublishedYear } = require('../utils/metadataUtils') const parseNameString = require('../utils/parsers/parseNameString') const parseEbookMetadata = require('../utils/parsers/parseEbookMetadata') const globals = require('../utils/globals') @@ -691,6 +692,9 @@ class BookScanner { } } + // Metadata precedence may leave a full ISO date in the year-only field. + bookMetadata.publishedYear = normalizePublishedYear(bookMetadata.publishedYear) + // Set cover from library file if one is found otherwise check audiofile if (libraryItemData.imageLibraryFiles.length) { const coverMatch = libraryItemData.imageLibraryFiles.find((iFile) => /\/cover\.[^.\/]*$/.test(iFile.metadata.path)) diff --git a/server/utils/metadataUtils.js b/server/utils/metadataUtils.js new file mode 100644 index 000000000..b077a49b8 --- /dev/null +++ b/server/utils/metadataUtils.js @@ -0,0 +1,14 @@ +/** + * Normalize ISO-style published dates to the book metadata year format. + * + * @param {*} value + * @returns {*} + */ +function normalizePublishedYear(value) { + if (typeof value !== 'string') return value + + const match = value.match(/^(\d{4})(?:-|$)/) + return match ? match[1] : value +} + +module.exports = { normalizePublishedYear } diff --git a/test/server/models/Book.test.js b/test/server/models/Book.test.js new file mode 100644 index 000000000..c5bbba85d --- /dev/null +++ b/test/server/models/Book.test.js @@ -0,0 +1,24 @@ +const { expect } = require('chai') +const sinon = require('sinon') +const Book = require('../../../server/models/Book') + +describe('Book', () => { + describe('updateFromRequest', () => { + it('normalizes an ISO date submitted as the published year', async () => { + const book = { + title: 'Test Book', + publishedYear: null, + save: sinon.stub().resolves(), + changed: sinon.stub().returns(['publishedYear']) + } + + const updated = await Book.prototype.updateFromRequest.call(book, { + metadata: { publishedYear: '2013-01-01' } + }) + + expect(updated).to.equal(true) + expect(book.publishedYear).to.equal('2013') + expect(book.save.calledOnce).to.equal(true) + }) + }) +}) diff --git a/test/server/scanner/BookScanner.test.js b/test/server/scanner/BookScanner.test.js new file mode 100644 index 000000000..ffd6c21e2 --- /dev/null +++ b/test/server/scanner/BookScanner.test.js @@ -0,0 +1,33 @@ +const chai = require('chai') +const expect = chai.expect +const BookScanner = require('../../../server/scanner/BookScanner') + +describe('BookScanner', () => { + let serverSettings + + beforeEach(() => { + serverSettings = global.ServerSettings + global.ServerSettings = { sortingPrefixes: [] } + }) + + afterEach(() => { + if (serverSettings === undefined) delete global.ServerSettings + else global.ServerSettings = serverSettings + }) + + describe('getBookMetadataFromScanData', () => { + it('normalizes an ISO date from audio metadata after sources are applied', async () => { + const libraryItemData = { + mediaMetadata: { title: 'Test Book' }, + imageLibraryFiles: [] + } + const libraryScan = { addLog() {} } + + const metadata = await BookScanner.getBookMetadataFromScanData([{ metaTags: { tagDate: '2013-01-01' }, chapters: [] }], null, libraryItemData, libraryScan, { + metadataPrecedence: ['audioMetatags'] + }) + + expect(metadata.publishedYear).to.equal('2013') + }) + }) +}) diff --git a/test/server/utils/metadataUtils.test.js b/test/server/utils/metadataUtils.test.js new file mode 100644 index 000000000..8075e2691 --- /dev/null +++ b/test/server/utils/metadataUtils.test.js @@ -0,0 +1,22 @@ +const chai = require('chai') +const expect = chai.expect +const { normalizePublishedYear } = require('../../../server/utils/metadataUtils') + +describe('normalizePublishedYear', () => { + it('keeps a four-digit year unchanged', () => { + expect(normalizePublishedYear('2013')).to.equal('2013') + }) + + it('extracts the year from ISO-style dates', () => { + expect(normalizePublishedYear('2013-01')).to.equal('2013') + expect(normalizePublishedYear('2013-01-01')).to.equal('2013') + expect(normalizePublishedYear('2013-01-01T12:34:56')).to.equal('2013') + }) + + it('leaves unsupported values unchanged', () => { + expect(normalizePublishedYear(null)).to.equal(null) + expect(normalizePublishedYear(2013)).to.equal(2013) + expect(normalizePublishedYear('01-01-2013')).to.equal('01-01-2013') + expect(normalizePublishedYear('2013/01/01')).to.equal('2013/01/01') + }) +})