mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-03 19:07:00 -05:00 
			
		
		
		
	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:
		
							parent
							
								
									72e59e77a7
								
							
						
					
					
						commit
						953ffe889e
					
				@ -9,6 +9,7 @@ class AudioMetaTags {
 | 
				
			|||||||
    this.tagTitleSort = null
 | 
					    this.tagTitleSort = null
 | 
				
			||||||
    this.tagSeries = null
 | 
					    this.tagSeries = null
 | 
				
			||||||
    this.tagSeriesPart = null
 | 
					    this.tagSeriesPart = null
 | 
				
			||||||
 | 
					    this.tagGrouping = null
 | 
				
			||||||
    this.tagTrack = null
 | 
					    this.tagTrack = null
 | 
				
			||||||
    this.tagDisc = null
 | 
					    this.tagDisc = null
 | 
				
			||||||
    this.tagSubtitle = null
 | 
					    this.tagSubtitle = null
 | 
				
			||||||
@ -116,6 +117,7 @@ class AudioMetaTags {
 | 
				
			|||||||
    this.tagTitleSort = metadata.tagTitleSort || null
 | 
					    this.tagTitleSort = metadata.tagTitleSort || null
 | 
				
			||||||
    this.tagSeries = metadata.tagSeries || null
 | 
					    this.tagSeries = metadata.tagSeries || null
 | 
				
			||||||
    this.tagSeriesPart = metadata.tagSeriesPart || null
 | 
					    this.tagSeriesPart = metadata.tagSeriesPart || null
 | 
				
			||||||
 | 
					    this.tagGrouping = metadata.tagGrouping || null
 | 
				
			||||||
    this.tagTrack = metadata.tagTrack || null
 | 
					    this.tagTrack = metadata.tagTrack || null
 | 
				
			||||||
    this.tagDisc = metadata.tagDisc || null
 | 
					    this.tagDisc = metadata.tagDisc || null
 | 
				
			||||||
    this.tagSubtitle = metadata.tagSubtitle || null
 | 
					    this.tagSubtitle = metadata.tagSubtitle || null
 | 
				
			||||||
@ -156,6 +158,7 @@ class AudioMetaTags {
 | 
				
			|||||||
    this.tagTitleSort = payload.file_tag_titlesort || null
 | 
					    this.tagTitleSort = payload.file_tag_titlesort || null
 | 
				
			||||||
    this.tagSeries = payload.file_tag_series || null
 | 
					    this.tagSeries = payload.file_tag_series || null
 | 
				
			||||||
    this.tagSeriesPart = payload.file_tag_seriespart || null
 | 
					    this.tagSeriesPart = payload.file_tag_seriespart || null
 | 
				
			||||||
 | 
					    this.tagGrouping = payload.file_tag_grouping || null
 | 
				
			||||||
    this.tagTrack = payload.file_tag_track || null
 | 
					    this.tagTrack = payload.file_tag_track || null
 | 
				
			||||||
    this.tagDisc = payload.file_tag_disc || null
 | 
					    this.tagDisc = payload.file_tag_disc || null
 | 
				
			||||||
    this.tagSubtitle = payload.file_tag_subtitle || null
 | 
					    this.tagSubtitle = payload.file_tag_subtitle || null
 | 
				
			||||||
@ -196,6 +199,7 @@ class AudioMetaTags {
 | 
				
			|||||||
      tagTitleSort: payload.file_tag_titlesort || null,
 | 
					      tagTitleSort: payload.file_tag_titlesort || null,
 | 
				
			||||||
      tagSeries: payload.file_tag_series || null,
 | 
					      tagSeries: payload.file_tag_series || null,
 | 
				
			||||||
      tagSeriesPart: payload.file_tag_seriespart || null,
 | 
					      tagSeriesPart: payload.file_tag_seriespart || null,
 | 
				
			||||||
 | 
					      tagGrouping: payload.file_tag_grouping || null,
 | 
				
			||||||
      tagTrack: payload.file_tag_track || null,
 | 
					      tagTrack: payload.file_tag_track || null,
 | 
				
			||||||
      tagDisc: payload.file_tag_disc || null,
 | 
					      tagDisc: payload.file_tag_disc || null,
 | 
				
			||||||
      tagSubtitle: payload.file_tag_subtitle || null,
 | 
					      tagSubtitle: payload.file_tag_subtitle || null,
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ const prober = require('../utils/prober')
 | 
				
			|||||||
const { LogLevel } = require('../utils/constants')
 | 
					const { LogLevel } = require('../utils/constants')
 | 
				
			||||||
const { parseOverdriveMediaMarkersAsChapters } = require('../utils/parsers/parseOverdriveMediaMarkers')
 | 
					const { parseOverdriveMediaMarkersAsChapters } = require('../utils/parsers/parseOverdriveMediaMarkers')
 | 
				
			||||||
const parseNameString = require('../utils/parsers/parseNameString')
 | 
					const parseNameString = require('../utils/parsers/parseNameString')
 | 
				
			||||||
 | 
					const parseSeriesString = require('../utils/parsers/parseSeriesString')
 | 
				
			||||||
const LibraryItem = require('../models/LibraryItem')
 | 
					const LibraryItem = require('../models/LibraryItem')
 | 
				
			||||||
const AudioFile = require('../objects/files/AudioFile')
 | 
					const AudioFile = require('../objects/files/AudioFile')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -256,6 +257,7 @@ class AudioFileScanner {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        tag: 'tagSeries',
 | 
					        tag: 'tagSeries',
 | 
				
			||||||
 | 
					        altTag: 'tagGrouping',
 | 
				
			||||||
        key: 'series'
 | 
					        key: 'series'
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
@ -276,8 +278,10 @@ class AudioFileScanner {
 | 
				
			|||||||
    const audioFileMetaTags = firstScannedFile.metaTags
 | 
					    const audioFileMetaTags = firstScannedFile.metaTags
 | 
				
			||||||
    MetadataMapArray.forEach((mapping) => {
 | 
					    MetadataMapArray.forEach((mapping) => {
 | 
				
			||||||
      let value = audioFileMetaTags[mapping.tag]
 | 
					      let value = audioFileMetaTags[mapping.tag]
 | 
				
			||||||
 | 
					      let isAltTag = false
 | 
				
			||||||
      if (!value && mapping.altTag) {
 | 
					      if (!value && mapping.altTag) {
 | 
				
			||||||
        value = audioFileMetaTags[mapping.altTag]
 | 
					        value = audioFileMetaTags[mapping.altTag]
 | 
				
			||||||
 | 
					        isAltTag = true
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (value && typeof value === 'string') {
 | 
					      if (value && typeof value === 'string') {
 | 
				
			||||||
@ -290,12 +294,28 @@ class AudioFileScanner {
 | 
				
			|||||||
        } else if (mapping.key === 'genres') {
 | 
					        } else if (mapping.key === 'genres') {
 | 
				
			||||||
          bookMetadata.genres = this.parseGenresString(value)
 | 
					          bookMetadata.genres = this.parseGenresString(value)
 | 
				
			||||||
        } else if (mapping.key === 'series') {
 | 
					        } else if (mapping.key === 'series') {
 | 
				
			||||||
          bookMetadata.series = [
 | 
					          // 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"
 | 
				
			||||||
              name: value,
 | 
					          if (isAltTag) {
 | 
				
			||||||
              sequence: audioFileMetaTags.tagSeriesPart || null
 | 
					            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 {
 | 
					        } else {
 | 
				
			||||||
          bookMetadata[mapping.key] = value
 | 
					          bookMetadata[mapping.key] = value
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -380,9 +380,8 @@ function getFFMetadataObject(libraryItem, audioFilesLength) {
 | 
				
			|||||||
    copyright: metadata.publisher,
 | 
					    copyright: metadata.publisher,
 | 
				
			||||||
    publisher: metadata.publisher, // mp3 only
 | 
					    publisher: metadata.publisher, // mp3 only
 | 
				
			||||||
    TRACKTOTAL: `${audioFilesLength}`, // 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) => {
 | 
					  Object.keys(ffmetadata).forEach((key) => {
 | 
				
			||||||
    if (!ffmetadata[key]) {
 | 
					    if (!ffmetadata[key]) {
 | 
				
			||||||
      delete ffmetadata[key]
 | 
					      delete ffmetadata[key]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
const Logger = require('../../Logger')
 | 
					const Logger = require('../../Logger')
 | 
				
			||||||
 | 
					const parseSeriesString = require('../parsers/parseSeriesString')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function parseJsonMetadataText(text) {
 | 
					function parseJsonMetadataText(text) {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
@ -19,39 +20,25 @@ function parseJsonMetadataText(text) {
 | 
				
			|||||||
    delete abmetadataData.metadata
 | 
					    delete abmetadataData.metadata
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (abmetadataData.series?.length) {
 | 
					    if (abmetadataData.series?.length) {
 | 
				
			||||||
      abmetadataData.series = [...new Set(abmetadataData.series.map(t => t?.trim()).filter(t => t))]
 | 
					      abmetadataData.series = [...new Set(abmetadataData.series.map((t) => t?.trim()).filter((t) => t))]
 | 
				
			||||||
      abmetadataData.series = abmetadataData.series.map(series => {
 | 
					      abmetadataData.series = abmetadataData.series.map((series) => parseSeriesString.parse(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
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // clean tags & remove dupes
 | 
					    // clean tags & remove dupes
 | 
				
			||||||
    if (abmetadataData.tags?.length) {
 | 
					    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) {
 | 
					    if (abmetadataData.chapters?.length) {
 | 
				
			||||||
      abmetadataData.chapters = cleanChaptersArray(abmetadataData.chapters, abmetadataData.title)
 | 
					      abmetadataData.chapters = cleanChaptersArray(abmetadataData.chapters, abmetadataData.title)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // clean remove dupes
 | 
					    // clean remove dupes
 | 
				
			||||||
    if (abmetadataData.authors?.length) {
 | 
					    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) {
 | 
					    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) {
 | 
					    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
 | 
					    return abmetadataData
 | 
				
			||||||
  } catch (error) {
 | 
					  } catch (error) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										27
									
								
								server/utils/parsers/parseSeriesString.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								server/utils/parsers/parseSeriesString.js
									
									
									
									
									
										Normal 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
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -189,6 +189,7 @@ function parseTags(format, verbose) {
 | 
				
			|||||||
    file_tag_genre: tryGrabTags(format, 'genre', 'tcon', 'tco'),
 | 
					    file_tag_genre: tryGrabTags(format, 'genre', 'tcon', 'tco'),
 | 
				
			||||||
    file_tag_series: tryGrabTags(format, 'series', 'show', 'mvnm'),
 | 
					    file_tag_series: tryGrabTags(format, 'series', 'show', 'mvnm'),
 | 
				
			||||||
    file_tag_seriespart: tryGrabTags(format, 'series-part', 'episode_id', 'mvin', 'part'),
 | 
					    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_isbn: tryGrabTags(format, 'isbn'), // custom
 | 
				
			||||||
    file_tag_language: tryGrabTags(format, 'language', 'lang'),
 | 
					    file_tag_language: tryGrabTags(format, 'language', 'lang'),
 | 
				
			||||||
    file_tag_asin: tryGrabTags(format, 'asin', 'audible_asin'), // custom
 | 
					    file_tag_asin: tryGrabTags(format, 'asin', 'audible_asin'), // custom
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user