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')
+ })
+})