Add:metadata.json format #1775 #916

This commit is contained in:
advplyr 2023-05-15 18:23:31 -05:00
parent 32bdae31a8
commit 81d4ac3ed2
8 changed files with 140 additions and 25 deletions

View File

@ -39,11 +39,15 @@
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" :label="$strings.LabelPrefixesToIgnore" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
</div>
<div class="flex items-center py-2">
<div class="flex items-center py-2 mb-2">
<ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
</div>
<div class="w-44 mb-2">
<ui-dropdown v-model="newServerSettings.metadataFileFormat" small :items="metadataFileFormats" label="Metadata File Format" @input="(val) => updateSettingsKey('metadataFileFormat', val)" :disabled="updatingServerSettings" />
</div>
<div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsDisplay }}</h2>
</div>
@ -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: {

View File

@ -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) {

View File

@ -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)

View File

@ -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)

View File

@ -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
}
})

View File

@ -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,

View File

@ -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
}

View File

@ -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