Update:Book series embeds in grouping meta tag as semicolon deliminated, book meta tag parser falls back to using grouping tag for series if set #3473

This commit is contained in:
advplyr 2024-10-20 16:58:13 -05:00
parent 72e59e77a7
commit 953ffe889e
6 changed files with 65 additions and 27 deletions

View File

@ -9,6 +9,7 @@ class AudioMetaTags {
this.tagTitleSort = null
this.tagSeries = null
this.tagSeriesPart = null
this.tagGrouping = null
this.tagTrack = null
this.tagDisc = null
this.tagSubtitle = null
@ -116,6 +117,7 @@ class AudioMetaTags {
this.tagTitleSort = metadata.tagTitleSort || null
this.tagSeries = metadata.tagSeries || null
this.tagSeriesPart = metadata.tagSeriesPart || null
this.tagGrouping = metadata.tagGrouping || null
this.tagTrack = metadata.tagTrack || null
this.tagDisc = metadata.tagDisc || null
this.tagSubtitle = metadata.tagSubtitle || null
@ -156,6 +158,7 @@ class AudioMetaTags {
this.tagTitleSort = payload.file_tag_titlesort || null
this.tagSeries = payload.file_tag_series || null
this.tagSeriesPart = payload.file_tag_seriespart || null
this.tagGrouping = payload.file_tag_grouping || null
this.tagTrack = payload.file_tag_track || null
this.tagDisc = payload.file_tag_disc || null
this.tagSubtitle = payload.file_tag_subtitle || null
@ -196,6 +199,7 @@ class AudioMetaTags {
tagTitleSort: payload.file_tag_titlesort || null,
tagSeries: payload.file_tag_series || null,
tagSeriesPart: payload.file_tag_seriespart || null,
tagGrouping: payload.file_tag_grouping || null,
tagTrack: payload.file_tag_track || null,
tagDisc: payload.file_tag_disc || null,
tagSubtitle: payload.file_tag_subtitle || null,

View File

@ -4,6 +4,7 @@ const prober = require('../utils/prober')
const { LogLevel } = require('../utils/constants')
const { parseOverdriveMediaMarkersAsChapters } = require('../utils/parsers/parseOverdriveMediaMarkers')
const parseNameString = require('../utils/parsers/parseNameString')
const parseSeriesString = require('../utils/parsers/parseSeriesString')
const LibraryItem = require('../models/LibraryItem')
const AudioFile = require('../objects/files/AudioFile')
@ -256,6 +257,7 @@ class AudioFileScanner {
},
{
tag: 'tagSeries',
altTag: 'tagGrouping',
key: 'series'
},
{
@ -276,8 +278,10 @@ class AudioFileScanner {
const audioFileMetaTags = firstScannedFile.metaTags
MetadataMapArray.forEach((mapping) => {
let value = audioFileMetaTags[mapping.tag]
let isAltTag = false
if (!value && mapping.altTag) {
value = audioFileMetaTags[mapping.altTag]
isAltTag = true
}
if (value && typeof value === 'string') {
@ -290,12 +294,28 @@ class AudioFileScanner {
} else if (mapping.key === 'genres') {
bookMetadata.genres = this.parseGenresString(value)
} else if (mapping.key === 'series') {
bookMetadata.series = [
{
name: value,
sequence: audioFileMetaTags.tagSeriesPart || null
// If series was embedded in the grouping tag, then parse it with semicolon separator and sequence in the same string
// e.g. "Test Series; Series Name #1; Other Series #2"
if (isAltTag) {
const series = value
.split(';')
.map((seriesWithPart) => {
seriesWithPart = seriesWithPart.trim()
return parseSeriesString.parse(seriesWithPart)
})
.filter(Boolean)
if (series.length) {
bookMetadata.series = series
}
]
} else {
// Original embed used "series" and "series-part" tags
bookMetadata.series = [
{
name: value,
sequence: audioFileMetaTags.tagSeriesPart || null
}
]
}
} else {
bookMetadata[mapping.key] = value
}

View File

@ -380,9 +380,8 @@ function getFFMetadataObject(libraryItem, audioFilesLength) {
copyright: metadata.publisher,
publisher: metadata.publisher, // mp3 only
TRACKTOTAL: `${audioFilesLength}`, // mp3 only
grouping: metadata.series?.map((s) => s.name + (s.sequence ? ` #${s.sequence}` : '')).join(', ')
grouping: metadata.series?.map((s) => s.name + (s.sequence ? ` #${s.sequence}` : '')).join('; ')
}
Object.keys(ffmetadata).forEach((key) => {
if (!ffmetadata[key]) {
delete ffmetadata[key]

View File

@ -1,4 +1,5 @@
const Logger = require('../../Logger')
const parseSeriesString = require('../parsers/parseSeriesString')
function parseJsonMetadataText(text) {
try {
@ -19,39 +20,25 @@ function parseJsonMetadataText(text) {
delete abmetadataData.metadata
if (abmetadataData.series?.length) {
abmetadataData.series = [...new Set(abmetadataData.series.map(t => t?.trim()).filter(t => t))]
abmetadataData.series = abmetadataData.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
}
})
abmetadataData.series = [...new Set(abmetadataData.series.map((t) => t?.trim()).filter((t) => t))]
abmetadataData.series = abmetadataData.series.map((series) => parseSeriesString.parse(series))
}
// clean tags & remove dupes
if (abmetadataData.tags?.length) {
abmetadataData.tags = [...new Set(abmetadataData.tags.map(t => t?.trim()).filter(t => t))]
abmetadataData.tags = [...new Set(abmetadataData.tags.map((t) => t?.trim()).filter((t) => t))]
}
if (abmetadataData.chapters?.length) {
abmetadataData.chapters = cleanChaptersArray(abmetadataData.chapters, abmetadataData.title)
}
// clean remove dupes
if (abmetadataData.authors?.length) {
abmetadataData.authors = [...new Set(abmetadataData.authors.map(t => t?.trim()).filter(t => t))]
abmetadataData.authors = [...new Set(abmetadataData.authors.map((t) => t?.trim()).filter((t) => t))]
}
if (abmetadataData.narrators?.length) {
abmetadataData.narrators = [...new Set(abmetadataData.narrators.map(t => t?.trim()).filter(t => t))]
abmetadataData.narrators = [...new Set(abmetadataData.narrators.map((t) => t?.trim()).filter((t) => t))]
}
if (abmetadataData.genres?.length) {
abmetadataData.genres = [...new Set(abmetadataData.genres.map(t => t?.trim()).filter(t => t))]
abmetadataData.genres = [...new Set(abmetadataData.genres.map((t) => t?.trim()).filter((t) => t))]
}
return abmetadataData
} catch (error) {

View File

@ -0,0 +1,27 @@
/**
* Parse a series string into a name and sequence
*
* @example
* Name #1a => { name: 'Name', sequence: '1a' }
* Name #1 => { name: 'Name', sequence: '1' }
*
* @param {string} seriesString
* @returns {{name: string, sequence: string}|null}
*/
module.exports.parse = (seriesString) => {
if (!seriesString || typeof seriesString !== 'string') return null
let sequence = null
let name = seriesString
// 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 = seriesString.match(/ #([^#\s]+)$/) // Pull out sequence #
if (matchResults && matchResults.length && matchResults.length > 1) {
sequence = matchResults[1] // Group 1
name = seriesString.replace(matchResults[0], '')
}
return {
name,
sequence
}
}

View File

@ -189,6 +189,7 @@ function parseTags(format, verbose) {
file_tag_genre: tryGrabTags(format, 'genre', 'tcon', 'tco'),
file_tag_series: tryGrabTags(format, 'series', 'show', 'mvnm'),
file_tag_seriespart: tryGrabTags(format, 'series-part', 'episode_id', 'mvin', 'part'),
file_tag_grouping: tryGrabTags(format, 'grouping'),
file_tag_isbn: tryGrabTags(format, 'isbn'), // custom
file_tag_language: tryGrabTags(format, 'language', 'lang'),
file_tag_asin: tryGrabTags(format, 'asin', 'audible_asin'), // custom