Update parsing and using tags from abmetadata file

This commit is contained in:
advplyr 2023-02-02 17:13:22 -06:00
parent 639b600570
commit 08f765fa51
3 changed files with 96 additions and 70 deletions

View File

@ -255,9 +255,16 @@ class Book {
const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'book') const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'book')
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) { if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
Logger.debug(`[Book] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates) Logger.debug(`[Book] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
if (abmetadataUpdates.tags) { // Set media tags if updated
this.tags = abmetadataUpdates.tags
tagsUpdated = true
}
if (abmetadataUpdates.metadata) {
metadataUpdatePayload = { metadataUpdatePayload = {
...metadataUpdatePayload, ...metadataUpdatePayload,
...abmetadataUpdates ...abmetadataUpdates.metadata
}
} }
} }
} }

View File

@ -188,22 +188,33 @@ class Podcast {
} }
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) { async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
var metadataUpdatePayload = {} let metadataUpdatePayload = {}
let tagsUpdated = false
var metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs') const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
if (metadataAbs) { if (metadataAbs) {
var metadataText = await readTextFile(metadataAbs.metadata.path) const metadataText = await readTextFile(metadataAbs.metadata.path)
var abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast') const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast')
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) { if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
Logger.debug(`[Podcast] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates) Logger.debug(`[Podcast] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
metadataUpdatePayload = abmetadataUpdates
if (abmetadataUpdates.tags) { // Set media tags if updated
this.tags = abmetadataUpdates.tags
tagsUpdated = true
}
if (abmetadataUpdates.metadata) {
metadataUpdatePayload = {
...metadataUpdatePayload,
...abmetadataUpdates.metadata
}
}
} }
} }
if (Object.keys(metadataUpdatePayload).length) { if (Object.keys(metadataUpdatePayload).length) {
return this.metadata.update(metadataUpdatePayload) return this.metadata.update(metadataUpdatePayload) || tagsUpdated
} }
return false return tagsUpdated
} }
searchEpisodes(query) { searchEpisodes(query) {

View File

@ -130,13 +130,13 @@ const metadataMappers = {
} }
function generate(libraryItem, outputPath) { function generate(libraryItem, outputPath) {
var fileString = `;ABMETADATA${CurrentAbMetadataVersion}\n` let fileString = `;ABMETADATA${CurrentAbMetadataVersion}\n`
fileString += `#audiobookshelf v${package.version}\n\n` fileString += `#audiobookshelf v${package.version}\n\n`
const mediaType = libraryItem.mediaType const mediaType = libraryItem.mediaType
fileString += `media=${mediaType}\n` fileString += `media=${mediaType}\n`
fileString += `tags=`+JSON.stringify(libraryItem.media.tags)+`\n\n` fileString += `tags=${JSON.stringify(libraryItem.media.tags)}\n`
const metadataMapper = metadataMappers[mediaType] const metadataMapper = metadataMappers[mediaType]
var mediaMetadata = libraryItem.media.metadata var mediaMetadata = libraryItem.media.metadata
@ -223,17 +223,31 @@ function parseChapterLines(lines) {
return chapter return chapter
} }
function parseTags(value) {
if (!value) return null
try {
const parsedTags = []
JSON.parse(value).forEach((loadedTag) => {
if (loadedTag.trim()) parsedTags.push(loadedTag) // Only push tags that are non-empty
})
return parsedTags
} catch (err) {
Logger.error(`[abmetadataGenerator] Error parsing TAGS "${value}":`, err.message)
return null
}
}
function parseAbMetadataText(text, mediaType) { function parseAbMetadataText(text, mediaType) {
if (!text) return null if (!text) return null
var lines = text.split(/\r?\n/) let lines = text.split(/\r?\n/)
// Check first line and get abmetadata version number // Check first line and get abmetadata version number
var firstLine = lines.shift().toLowerCase() const firstLine = lines.shift().toLowerCase()
if (!firstLine.startsWith(';abmetadata')) { if (!firstLine.startsWith(';abmetadata')) {
Logger.error(`Invalid abmetadata file first line is not ;abmetadata "${firstLine}"`) Logger.error(`Invalid abmetadata file first line is not ;abmetadata "${firstLine}"`)
return null return null
} }
var abmetadataVersion = Number(firstLine.replace(';abmetadata', '').trim()) const abmetadataVersion = Number(firstLine.replace(';abmetadata', '').trim())
if (isNaN(abmetadataVersion) || abmetadataVersion != CurrentAbMetadataVersion) { if (isNaN(abmetadataVersion) || abmetadataVersion != CurrentAbMetadataVersion) {
Logger.warn(`Invalid abmetadata version ${abmetadataVersion} - must use version ${CurrentAbMetadataVersion}`) Logger.warn(`Invalid abmetadata version ${abmetadataVersion} - must use version ${CurrentAbMetadataVersion}`)
return null return null
@ -244,9 +258,9 @@ function parseAbMetadataText(text, mediaType) {
lines = lines.filter(line => !!line.trim() && !ignoreFirstChars.includes(line[0])) lines = lines.filter(line => !!line.trim() && !ignoreFirstChars.includes(line[0]))
// Get lines that map to book details (all lines before the first chapter or description section) // Get lines that map to book details (all lines before the first chapter or description section)
var firstSectionLine = lines.findIndex(l => l.startsWith('[')) const firstSectionLine = lines.findIndex(l => l.startsWith('['))
var detailLines = firstSectionLine > 0 ? lines.slice(0, firstSectionLine) : lines const detailLines = firstSectionLine > 0 ? lines.slice(0, firstSectionLine) : lines
var remainingLines = firstSectionLine > 0 ? lines.slice(firstSectionLine) : [] const remainingLines = firstSectionLine > 0 ? lines.slice(firstSectionLine) : []
if (!detailLines.length) { if (!detailLines.length) {
Logger.error(`Invalid abmetadata file no detail lines`) Logger.error(`Invalid abmetadata file no detail lines`)
@ -255,64 +269,57 @@ function parseAbMetadataText(text, mediaType) {
// Check the media type saved for this abmetadata file show warning if not matching expected // Check the media type saved for this abmetadata file show warning if not matching expected
if (detailLines[0].toLowerCase().startsWith('media=')) { if (detailLines[0].toLowerCase().startsWith('media=')) {
var mediaLine = detailLines.shift() // Remove media line const mediaLine = detailLines.shift() // Remove media line
var abMediaType = mediaLine.toLowerCase().split('=')[1].trim() const abMediaType = mediaLine.toLowerCase().split('=')[1].trim()
if (abMediaType != mediaType) { if (abMediaType != mediaType) {
Logger.warn(`Invalid media type in abmetadata file ${abMediaType} expecting ${mediaType}`) Logger.warn(`Invalid media type in abmetadata file ${abMediaType} expecting ${mediaType}`)
} }
} else { } else {
Logger.warn(`No media type found in abmetadata file - expecting ${mediaType}`) Logger.warn(`No media type found in abmetadata file - expecting ${mediaType}`)
} }
const abTags = [];
try{
if (detailLines[0].toLowerCase().startsWith('tags=')) {
var tagLine = detailLines.shift()
var tagsStr = tagLine.substring(5, tagLine.len)
JSON.parse(tagsStr).forEach((loadedTag) => { abTags.push(loadedTag) })
}
}catch(err){
Logger.error("Error parsing TAGS:", err.message)
}
const metadataMapper = metadataMappers[mediaType] const metadataMapper = metadataMappers[mediaType]
// Put valid book detail values into map // Put valid book detail values into map
const mediaMetadataDetails = {} const mediaDetails = {
metadata: {},
chapters: [],
tags: null // When tags are null it will not be used
}
for (let i = 0; i < detailLines.length; i++) { for (let i = 0; i < detailLines.length; i++) {
var line = detailLines[i] const line = detailLines[i]
var keyValue = line.split('=') const keyValue = line.split('=')
if (keyValue.length < 2) { if (keyValue.length < 2) {
Logger.warn('abmetadata invalid line has no =', line) Logger.warn('abmetadata invalid line has no =', line)
} else if (!metadataMapper[keyValue[0].trim()]) { } else if (keyValue[0].trim() === 'tags') { // Parse tags
const value = keyValue.slice(1).join('=').trim() // Everything after "tags="
mediaDetails.tags = parseTags(value)
} else if (!metadataMapper[keyValue[0].trim()]) { // Ensure valid media metadata key
Logger.warn(`abmetadata key "${keyValue[0].trim()}" is not a valid ${mediaType} metadata key`) Logger.warn(`abmetadata key "${keyValue[0].trim()}" is not a valid ${mediaType} metadata key`)
} else { } else {
var key = keyValue.shift().trim() const key = keyValue.shift().trim()
var value = keyValue.join('=').trim() const value = keyValue.join('=').trim()
mediaMetadataDetails[key] = metadataMapper[key].from(value) mediaDetails.metadata[key] = metadataMapper[key].from(value)
} }
} }
const chapters = []
// Parse sections for description and chapters // Parse sections for description and chapters
var sections = parseSections(remainingLines) const sections = parseSections(remainingLines)
sections.forEach((section) => { sections.forEach((section) => {
var sectionHeader = section.shift() const sectionHeader = section.shift()
if (sectionHeader.toLowerCase().startsWith('[description]')) { if (sectionHeader.toLowerCase().startsWith('[description]')) {
mediaMetadataDetails.description = section.join('\n') mediaDetails.metadata.description = section.join('\n')
} else if (sectionHeader.toLowerCase().startsWith('[chapter]')) { } else if (sectionHeader.toLowerCase().startsWith('[chapter]')) {
var chapter = parseChapterLines(section) const chapter = parseChapterLines(section)
if (chapter) { if (chapter) {
chapters.push(chapter) mediaDetails.chapters.push(chapter)
} }
} }
}) })
chapters.sort((a, b) => a.start - b.start) mediaDetails.chapters.sort((a, b) => a.start - b.start)
return { return mediaDetails
metadata: mediaMetadataDetails,
chapters,
tags: abTags
}
} }
module.exports.parse = parseAbMetadataText module.exports.parse = parseAbMetadataText
@ -386,53 +393,54 @@ function checkArraysChanged(abmetadataArray, mediaArray) {
return abmetadataArray.join(',') != mediaArray.join(',') return abmetadataArray.join(',') != mediaArray.join(',')
} }
// Input text from abmetadata file and return object of metadata changes from media metadata // 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) {
if (!text || !media || !media.metadata || !mediaType) { if (!text || !media || !media.metadata || !mediaType) {
Logger.error(`Invalid inputs to parseAndCheckForUpdates`) Logger.error(`Invalid inputs to parseAndCheckForUpdates`)
return null return null
} }
const mediaMetadata = media.metadata const mediaMetadata = media.metadata
var updatePayload = {} // Only updated key/values const metadataUpdatePayload = {} // Only updated key/values
var abmetadataData = parseAbMetadataText(text, mediaType) const abmetadataData = parseAbMetadataText(text, mediaType)
if (!abmetadataData || !abmetadataData.metadata) { if (!abmetadataData || !abmetadataData.metadata) {
return null return null
} }
var abMetadata = abmetadataData.metadata // Metadata from abmetadata file const abMetadata = abmetadataData.metadata // Metadata from abmetadata file
for (const key in abMetadata) { for (const key in abMetadata) {
if (mediaMetadata[key] !== undefined) { if (mediaMetadata[key] !== undefined) {
if (key === 'authors') { if (key === 'authors') {
var authorUpdatePayload = checkUpdatedBookAuthors(abMetadata[key], mediaMetadata[key]) const authorUpdatePayload = checkUpdatedBookAuthors(abMetadata[key], mediaMetadata[key])
if (authorUpdatePayload.hasUpdates) updatePayload.authors = authorUpdatePayload.authors if (authorUpdatePayload.hasUpdates) metadataUpdatePayload.authors = authorUpdatePayload.authors
} else if (key === 'series') { } else if (key === 'series') {
var seriesUpdatePayload = checkUpdatedBookSeries(abMetadata[key], mediaMetadata[key]) const seriesUpdatePayload = checkUpdatedBookSeries(abMetadata[key], mediaMetadata[key])
if (seriesUpdatePayload.hasUpdates) updatePayload.series = seriesUpdatePayload.series if (seriesUpdatePayload.hasUpdates) metadataUpdatePayload.series = seriesUpdatePayload.series
} else if (key === 'genres' || key === 'narrators') { // Compare array differences } else if (key === 'genres' || key === 'narrators') { // Compare array differences
if (checkArraysChanged(abMetadata[key], mediaMetadata[key])) { if (checkArraysChanged(abMetadata[key], mediaMetadata[key])) {
updatePayload[key] = abMetadata[key] metadataUpdatePayload[key] = abMetadata[key]
} }
} else if (abMetadata[key] !== mediaMetadata[key]) { } else if (abMetadata[key] !== mediaMetadata[key]) {
updatePayload[key] = abMetadata[key] metadataUpdatePayload[key] = abMetadata[key]
} }
} else { } else {
Logger.warn('[abmetadataGenerator] Invalid key', key) Logger.warn('[abmetadataGenerator] Invalid key', key)
} }
} }
try{
if(abmetadataData.tags.length > 0){ const updatePayload = {} // Only updated key/values
abmetadataData.tags.forEach((tag) => { // Check update tags
if(media.tags.includes(tag) == false){ if (abmetadataData.tags) {
media.tags.push(copyValue(tag)) if (checkArraysChanged(abmetadataData.tags, media.tags)) {
} updatePayload.tags = abmetadataData.tags
})
} }
} }
catch(err){
Logger.error("[abmetadataGenerator] Error parsing tags", err.message) if (Object.keys(metadataUpdatePayload).length) {
updatePayload.metadata = metadataUpdatePayload
} }
return updatePayload return updatePayload
} }
module.exports.parseAndCheckForUpdates = parseAndCheckForUpdates module.exports.parseAndCheckForUpdates = parseAndCheckForUpdates