diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue
index 7f0170c7..93a4f803 100644
--- a/client/pages/config/index.vue
+++ b/client/pages/config/index.vue
@@ -39,11 +39,15 @@
-
+
updateSettingsKey('chromecastEnabled', val)" />
{{ $strings.LabelSettingsChromecastSupport }}
+
+ updateSettingsKey('metadataFileFormat', val)" :disabled="updatingServerSettings" />
+
+
{{ $strings.HeaderSettingsDisplay }}
@@ -272,7 +276,17 @@ export default {
useBookshelfView: false,
isPurgingCache: false,
newServerSettings: {},
- showConfirmPurgeCache: false
+ showConfirmPurgeCache: false,
+ metadataFileFormats: [
+ {
+ text: '.json',
+ value: 'json'
+ },
+ {
+ text: '.abs',
+ value: 'abs'
+ }
+ ]
}
},
watch: {
diff --git a/server/objects/LibraryItem.js b/server/objects/LibraryItem.js
index b15e53c9..0d001799 100644
--- a/server/objects/LibraryItem.js
+++ b/server/objects/LibraryItem.js
@@ -499,14 +499,37 @@ class LibraryItem {
// Make sure metadata book dir exists
await fs.ensureDir(metadataPath)
}
- metadataPath = Path.join(metadataPath, 'metadata.abs')
- return abmetadataGenerator.generate(this, metadataPath).then((success) => {
- this.isSavingMetadata = false
- if (!success) Logger.error(`[LibraryItem] Failed saving abmetadata to "${metadataPath}"`)
- else Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataPath}"`)
- return success
- })
+ const metadataFileFormat = global.ServerSettings.metadataFileFormat
+ const metadataFilePath = Path.join(metadataPath, `metadata.${metadataFileFormat}`)
+
+ if (metadataFileFormat === 'json') {
+ // Remove metadata.abs if it exists
+ if (await fs.pathExists(Path.join(metadataPath, `metadata.abs`))) {
+ Logger.debug(`[LibraryItem] Removing metadata.abs for item "${this.media.metadata.title}"`)
+ await fs.remove(Path.join(metadataPath, `metadata.abs`))
+ }
+
+ return fs.writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2)).then(() => {
+ return true
+ }).catch((error) => {
+ Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error)
+ return false
+ })
+ } else {
+ // Remove metadata.json if it exists
+ if (await fs.pathExists(Path.join(metadataPath, `metadata.json`))) {
+ Logger.debug(`[LibraryItem] Removing metadata.json for item "${this.media.metadata.title}"`)
+ await fs.remove(Path.join(metadataPath, `metadata.json`))
+ }
+
+ return abmetadataGenerator.generate(this, metadataFilePath).then((success) => {
+ this.isSavingMetadata = false
+ if (!success) Logger.error(`[LibraryItem] Failed saving abmetadata to "${metadataFilePath}"`)
+ else Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
+ return success
+ })
+ }
}
removeLibraryFile(ino) {
diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js
index 18c6ea52..d0cfab93 100644
--- a/server/objects/mediaTypes/Book.js
+++ b/server/objects/mediaTypes/Book.js
@@ -89,6 +89,14 @@ class Book {
}
}
+ toJSONForMetadataFile() {
+ return {
+ tags: [...this.tags],
+ chapters: this.chapters.map(c => ({ ...c })),
+ metadata: this.metadata.toJSONForMetadataFile()
+ }
+ }
+
get size() {
var total = 0
this.audioFiles.forEach((af) => total += af.metadata.size)
@@ -248,11 +256,12 @@ class Book {
}
}
- const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
+ const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs' || lf.metadata.filename === 'metadata.json')
if (metadataAbs) {
- Logger.debug(`[Book] Found metadata.abs file for "${this.metadata.title}"`)
+ const isJSON = metadataAbs.metadata.filename === 'metadata.json'
+ Logger.debug(`[Book] Found ${metadataAbs.metadata.filename} file for "${this.metadata.title}"`)
const metadataText = await readTextFile(metadataAbs.metadata.path)
- const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'book')
+ const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'book', isJSON)
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
Logger.debug(`[Book] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js
index 46c34a39..e1227deb 100644
--- a/server/objects/mediaTypes/Podcast.js
+++ b/server/objects/mediaTypes/Podcast.js
@@ -94,6 +94,13 @@ class Podcast {
}
}
+ toJSONForMetadataFile() {
+ return {
+ tags: [...this.tags],
+ metadata: this.metadata.toJSON()
+ }
+ }
+
get size() {
var total = 0
this.episodes.forEach((ep) => total += ep.size)
@@ -199,10 +206,11 @@ class Podcast {
let metadataUpdatePayload = {}
let tagsUpdated = false
- const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
+ const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs' || lf.metadata.filename === 'metadata.json')
if (metadataAbs) {
+ const isJSON = metadataAbs.metadata.filename === 'metadata.json'
const metadataText = await readTextFile(metadataAbs.metadata.path)
- const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast')
+ const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast', isJSON)
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
Logger.debug(`[Podcast] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
diff --git a/server/objects/metadata/BookMetadata.js b/server/objects/metadata/BookMetadata.js
index 586d699a..7cdbdcff 100644
--- a/server/objects/metadata/BookMetadata.js
+++ b/server/objects/metadata/BookMetadata.js
@@ -109,6 +109,16 @@ class BookMetadata {
}
}
+ toJSONForMetadataFile() {
+ const json = this.toJSON()
+ json.authors = json.authors.map(au => au.name)
+ json.series = json.series.map(se => {
+ if (!se.sequence) return se.name
+ return `${se.name} #${se.sequence}`
+ })
+ return json
+ }
+
clone() {
return new BookMetadata(this.toJSON())
}
@@ -191,8 +201,9 @@ class BookMetadata {
}
update(payload) {
- var json = this.toJSON()
- var hasUpdates = false
+ const json = this.toJSON()
+ let hasUpdates = false
+
for (const key in json) {
if (payload[key] !== undefined) {
if (!areEquivalent(payload[key], json[key])) {
@@ -373,8 +384,10 @@ class BookMetadata {
const parsed = parseNameString.parse(authorsTag)
if (!parsed) return []
return (parsed.names || []).map((au) => {
+ const findAuthor = this.authors.find(_au => _au.name == au)
+
return {
- id: `new-${Math.floor(Math.random() * 1000000)}`,
+ id: findAuthor?.id || `new-${Math.floor(Math.random() * 1000000)}`,
name: au
}
})
diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js
index 1dbd3417..16855c38 100644
--- a/server/objects/settings/ServerSettings.js
+++ b/server/objects/settings/ServerSettings.js
@@ -1,5 +1,4 @@
const { BookshelfView } = require('../../utils/constants')
-const { isNullOrNaN } = require('../../utils')
const Logger = require('../../Logger')
class ServerSettings {
@@ -21,6 +20,7 @@ class ServerSettings {
// Metadata - choose to store inside users library item folder
this.storeCoverWithItem = false
this.storeMetadataWithItem = false
+ this.metadataFileFormat = 'json'
// Security/Rate limits
this.rateLimitLoginRequests = 10
@@ -77,6 +77,7 @@ class ServerSettings {
this.storeCoverWithItem = !!settings.storeCoverWithItem
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
+ this.metadataFileFormat = settings.metadataFileFormat || 'json'
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
@@ -112,6 +113,16 @@ class ServerSettings {
if (settings.homeBookshelfView == undefined) { // homeBookshelfView was added in 2.1.3
this.homeBookshelfView = settings.bookshelfView
}
+ if (settings.metadataFileFormat == undefined) { // metadataFileFormat was added in 2.2.21
+ // All users using old settings will stay abs until changed
+ this.metadataFileFormat = 'abs'
+ }
+
+ // Validation
+ if (!['abs', 'json'].includes(this.metadataFileFormat)) {
+ Logger.error(`[ServerSettings] construct: Invalid metadataFileFormat ${this.metadataFileFormat}`)
+ this.metadataFileFormat = 'json'
+ }
if (this.logLevel !== Logger.logLevel) {
Logger.setLogLevel(this.logLevel)
@@ -133,6 +144,7 @@ class ServerSettings {
scannerUseTone: this.scannerUseTone,
storeCoverWithItem: this.storeCoverWithItem,
storeMetadataWithItem: this.storeMetadataWithItem,
+ metadataFileFormat: this.metadataFileFormat,
rateLimitLoginRequests: this.rateLimitLoginRequests,
rateLimitLoginWindow: this.rateLimitLoginWindow,
backupSchedule: this.backupSchedule,
diff --git a/server/utils/abmetadataGenerator.js b/server/utils/abmetadataGenerator.js
index e6fb0b70..26cffbae 100644
--- a/server/utils/abmetadataGenerator.js
+++ b/server/utils/abmetadataGenerator.js
@@ -2,7 +2,7 @@ const fs = require('../libs/fsExtra')
const filePerms = require('./filePerms')
const package = require('../../package.json')
const Logger = require('../Logger')
-const { getId, copyValue } = require('./index')
+const { getId } = require('./index')
const CurrentAbMetadataVersion = 2
@@ -328,11 +328,11 @@ function parseAbMetadataText(text, mediaType) {
module.exports.parse = parseAbMetadataText
function checkUpdatedBookAuthors(abmetadataAuthors, authors) {
- var finalAuthors = []
- var hasUpdates = false
+ const finalAuthors = []
+ let hasUpdates = false
abmetadataAuthors.forEach((authorName) => {
- var findAuthor = authors.find(au => au.name.toLowerCase() == authorName.toLowerCase())
+ const findAuthor = authors.find(au => au.name.toLowerCase() == authorName.toLowerCase())
if (!findAuthor) {
hasUpdates = true
finalAuthors.push({
@@ -397,18 +397,54 @@ function checkArraysChanged(abmetadataArray, mediaArray) {
return abmetadataArray.join(',') != mediaArray.join(',')
}
+function parseJsonMetadataText(text) {
+ try {
+ const abmetadataData = JSON.parse(text)
+ if (abmetadataData.metadata?.series?.length) {
+ abmetadataData.metadata.series = abmetadataData.metadata.series.map(series => {
+ let sequence = null
+ let name = series
+ // Series sequence match any characters after " #" other than whitespace and another #
+ // e.g. "Name #1a" is valid. "Name #1#a" or "Name #1 a" is not valid.
+ const matchResults = series.match(/ #([^#\s]+)$/) // Pull out sequence #
+ if (matchResults && matchResults.length && matchResults.length > 1) {
+ sequence = matchResults[1] // Group 1
+ name = series.replace(matchResults[0], '')
+ }
+ return {
+ name,
+ sequence
+ }
+ })
+ }
+ return abmetadataData
+ } catch (error) {
+ Logger.error(`[abmetadataGenerator] Invalid metadata.json JSON`, error)
+ return null
+ }
+}
+
// Input text from abmetadata file and return object of media changes
// only returns object of changes. empty object means no changes
-function parseAndCheckForUpdates(text, media, mediaType) {
+function parseAndCheckForUpdates(text, media, mediaType, isJSON) {
if (!text || !media || !media.metadata || !mediaType) {
Logger.error(`Invalid inputs to parseAndCheckForUpdates`)
return null
}
+
const mediaMetadata = media.metadata
const metadataUpdatePayload = {} // Only updated key/values
- const abmetadataData = parseAbMetadataText(text, mediaType)
+ let abmetadataData = null
+
+ if (isJSON) {
+ abmetadataData = parseJsonMetadataText(text)
+ } else {
+ abmetadataData = parseAbMetadataText(text, mediaType)
+ }
+
if (!abmetadataData || !abmetadataData.metadata) {
+ Logger.error(`[abmetadataGenerator] Invalid metadata file`)
return null
}
diff --git a/server/utils/globals.js b/server/utils/globals.js
index 71c1c2c0..7d20dfe3 100644
--- a/server/utils/globals.js
+++ b/server/utils/globals.js
@@ -4,7 +4,7 @@ const globals = {
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
SupportedVideoTypes: ['mp4'],
TextFileTypes: ['txt', 'nfo'],
- MetadataFileTypes: ['opf', 'abs', 'xml']
+ MetadataFileTypes: ['opf', 'abs', 'xml', 'json']
}
module.exports = globals