From 08f765fa518d4e241505f5c7a5bd17ef17ac1073 Mon Sep 17 00:00:00 2001 From: advplyr Date: Thu, 2 Feb 2023 17:13:22 -0600 Subject: [PATCH] Update parsing and using tags from abmetadata file --- server/objects/mediaTypes/Book.js | 13 ++- server/objects/mediaTypes/Podcast.js | 25 ++++-- server/utils/abmetadataGenerator.js | 128 ++++++++++++++------------- 3 files changed, 96 insertions(+), 70 deletions(-) diff --git a/server/objects/mediaTypes/Book.js b/server/objects/mediaTypes/Book.js index 321d0170..3fa4c1be 100644 --- a/server/objects/mediaTypes/Book.js +++ b/server/objects/mediaTypes/Book.js @@ -255,9 +255,16 @@ class Book { const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'book') if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) { Logger.debug(`[Book] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates) - metadataUpdatePayload = { - ...metadataUpdatePayload, - ...abmetadataUpdates + + if (abmetadataUpdates.tags) { // Set media tags if updated + this.tags = abmetadataUpdates.tags + tagsUpdated = true + } + if (abmetadataUpdates.metadata) { + metadataUpdatePayload = { + ...metadataUpdatePayload, + ...abmetadataUpdates.metadata + } } } } diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js index f279be88..2f45cc4f 100644 --- a/server/objects/mediaTypes/Podcast.js +++ b/server/objects/mediaTypes/Podcast.js @@ -188,22 +188,33 @@ class Podcast { } 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) { - var metadataText = await readTextFile(metadataAbs.metadata.path) - var abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast') + const metadataText = await readTextFile(metadataAbs.metadata.path) + const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast') if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) { 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) { - return this.metadata.update(metadataUpdatePayload) + return this.metadata.update(metadataUpdatePayload) || tagsUpdated } - return false + return tagsUpdated } searchEpisodes(query) { diff --git a/server/utils/abmetadataGenerator.js b/server/utils/abmetadataGenerator.js index be343979..bf638492 100644 --- a/server/utils/abmetadataGenerator.js +++ b/server/utils/abmetadataGenerator.js @@ -130,13 +130,13 @@ const metadataMappers = { } function generate(libraryItem, outputPath) { - var fileString = `;ABMETADATA${CurrentAbMetadataVersion}\n` + let fileString = `;ABMETADATA${CurrentAbMetadataVersion}\n` fileString += `#audiobookshelf v${package.version}\n\n` const mediaType = libraryItem.mediaType 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] var mediaMetadata = libraryItem.media.metadata @@ -223,17 +223,31 @@ function parseChapterLines(lines) { 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) { if (!text) return null - var lines = text.split(/\r?\n/) + let lines = text.split(/\r?\n/) // Check first line and get abmetadata version number - var firstLine = lines.shift().toLowerCase() + const firstLine = lines.shift().toLowerCase() if (!firstLine.startsWith(';abmetadata')) { Logger.error(`Invalid abmetadata file first line is not ;abmetadata "${firstLine}"`) return null } - var abmetadataVersion = Number(firstLine.replace(';abmetadata', '').trim()) + const abmetadataVersion = Number(firstLine.replace(';abmetadata', '').trim()) if (isNaN(abmetadataVersion) || abmetadataVersion != CurrentAbMetadataVersion) { Logger.warn(`Invalid abmetadata version ${abmetadataVersion} - must use version ${CurrentAbMetadataVersion}`) return null @@ -244,9 +258,9 @@ function parseAbMetadataText(text, mediaType) { 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) - var firstSectionLine = lines.findIndex(l => l.startsWith('[')) - var detailLines = firstSectionLine > 0 ? lines.slice(0, firstSectionLine) : lines - var remainingLines = firstSectionLine > 0 ? lines.slice(firstSectionLine) : [] + const firstSectionLine = lines.findIndex(l => l.startsWith('[')) + const detailLines = firstSectionLine > 0 ? lines.slice(0, firstSectionLine) : lines + const remainingLines = firstSectionLine > 0 ? lines.slice(firstSectionLine) : [] if (!detailLines.length) { 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 if (detailLines[0].toLowerCase().startsWith('media=')) { - var mediaLine = detailLines.shift() // Remove media line - var abMediaType = mediaLine.toLowerCase().split('=')[1].trim() + const mediaLine = detailLines.shift() // Remove media line + const abMediaType = mediaLine.toLowerCase().split('=')[1].trim() if (abMediaType != mediaType) { Logger.warn(`Invalid media type in abmetadata file ${abMediaType} expecting ${mediaType}`) } } else { 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] // 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++) { - var line = detailLines[i] - var keyValue = line.split('=') + const line = detailLines[i] + const keyValue = line.split('=') if (keyValue.length < 2) { 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`) } else { - var key = keyValue.shift().trim() - var value = keyValue.join('=').trim() - mediaMetadataDetails[key] = metadataMapper[key].from(value) + const key = keyValue.shift().trim() + const value = keyValue.join('=').trim() + mediaDetails.metadata[key] = metadataMapper[key].from(value) } } - const chapters = [] - // Parse sections for description and chapters - var sections = parseSections(remainingLines) + const sections = parseSections(remainingLines) sections.forEach((section) => { - var sectionHeader = section.shift() + const sectionHeader = section.shift() if (sectionHeader.toLowerCase().startsWith('[description]')) { - mediaMetadataDetails.description = section.join('\n') + mediaDetails.metadata.description = section.join('\n') } else if (sectionHeader.toLowerCase().startsWith('[chapter]')) { - var chapter = parseChapterLines(section) + const chapter = parseChapterLines(section) 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 { - metadata: mediaMetadataDetails, - chapters, - tags: abTags - } + return mediaDetails } module.exports.parse = parseAbMetadataText @@ -386,53 +393,54 @@ function checkArraysChanged(abmetadataArray, mediaArray) { 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) { if (!text || !media || !media.metadata || !mediaType) { Logger.error(`Invalid inputs to parseAndCheckForUpdates`) return null } 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) { return null } - var abMetadata = abmetadataData.metadata // Metadata from abmetadata file - + const abMetadata = abmetadataData.metadata // Metadata from abmetadata file for (const key in abMetadata) { if (mediaMetadata[key] !== undefined) { if (key === 'authors') { - var authorUpdatePayload = checkUpdatedBookAuthors(abMetadata[key], mediaMetadata[key]) - if (authorUpdatePayload.hasUpdates) updatePayload.authors = authorUpdatePayload.authors + const authorUpdatePayload = checkUpdatedBookAuthors(abMetadata[key], mediaMetadata[key]) + if (authorUpdatePayload.hasUpdates) metadataUpdatePayload.authors = authorUpdatePayload.authors } else if (key === 'series') { - var seriesUpdatePayload = checkUpdatedBookSeries(abMetadata[key], mediaMetadata[key]) - if (seriesUpdatePayload.hasUpdates) updatePayload.series = seriesUpdatePayload.series + const seriesUpdatePayload = checkUpdatedBookSeries(abMetadata[key], mediaMetadata[key]) + if (seriesUpdatePayload.hasUpdates) metadataUpdatePayload.series = seriesUpdatePayload.series } else if (key === 'genres' || key === 'narrators') { // Compare array differences if (checkArraysChanged(abMetadata[key], mediaMetadata[key])) { - updatePayload[key] = abMetadata[key] + metadataUpdatePayload[key] = abMetadata[key] } } else if (abMetadata[key] !== mediaMetadata[key]) { - updatePayload[key] = abMetadata[key] + metadataUpdatePayload[key] = abMetadata[key] } } else { Logger.warn('[abmetadataGenerator] Invalid key', key) } } - try{ - if(abmetadataData.tags.length > 0){ - abmetadataData.tags.forEach((tag) => { - if(media.tags.includes(tag) == false){ - media.tags.push(copyValue(tag)) - } - }) + + const updatePayload = {} // Only updated key/values + // Check update tags + if (abmetadataData.tags) { + 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 } module.exports.parseAndCheckForUpdates = parseAndCheckForUpdates