mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add NFO metadata source
This commit is contained in:
		
							parent
							
								
									d3a55c8b1a
								
							
						
					
					
						commit
						d990e5b909
					
				@ -127,7 +127,7 @@ export default {
 | 
			
		||||
          skipMatchingMediaWithIsbn: false,
 | 
			
		||||
          autoScanCronExpression: null,
 | 
			
		||||
          hideSingleBookSeries: false,
 | 
			
		||||
          metadataPrecedence: ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata']
 | 
			
		||||
          metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
@ -64,6 +64,11 @@ export default {
 | 
			
		||||
          name: 'Audio file meta tags',
 | 
			
		||||
          include: true
 | 
			
		||||
        },
 | 
			
		||||
        nfoFile: {
 | 
			
		||||
          id: 'nfoFile',
 | 
			
		||||
          name: 'NFO file',
 | 
			
		||||
          include: true
 | 
			
		||||
        },
 | 
			
		||||
        txtFiles: {
 | 
			
		||||
          id: 'txtFiles',
 | 
			
		||||
          name: 'desc.txt & reader.txt files',
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ class LibrarySettings {
 | 
			
		||||
    this.autoScanCronExpression = null
 | 
			
		||||
    this.audiobooksOnly = false
 | 
			
		||||
    this.hideSingleBookSeries = false // Do not show series that only have 1 book 
 | 
			
		||||
    this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata']
 | 
			
		||||
    this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
 | 
			
		||||
 | 
			
		||||
    if (settings) {
 | 
			
		||||
      this.construct(settings)
 | 
			
		||||
@ -28,7 +28,7 @@ class LibrarySettings {
 | 
			
		||||
      this.metadataPrecedence = [...settings.metadataPrecedence]
 | 
			
		||||
    } else {
 | 
			
		||||
      // Added in v2.4.5
 | 
			
		||||
      this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata']
 | 
			
		||||
      this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ const BookFinder = require('../finders/BookFinder')
 | 
			
		||||
 | 
			
		||||
const LibraryScan = require("./LibraryScan")
 | 
			
		||||
const OpfFileScanner = require('./OpfFileScanner')
 | 
			
		||||
const NfoFileScanner = require('./NfoFileScanner')
 | 
			
		||||
const AbsMetadataFileScanner = require('./AbsMetadataFileScanner')
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@ -593,7 +594,7 @@ class BookScanner {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const bookMetadataSourceHandler = new BookScanner.BookMetadataSourceHandler(bookMetadata, audioFiles, libraryItemData, libraryScan, existingLibraryItemId)
 | 
			
		||||
    const metadataPrecedence = librarySettings.metadataPrecedence || ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata']
 | 
			
		||||
    const metadataPrecedence = librarySettings.metadataPrecedence || ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
 | 
			
		||||
    libraryScan.addLog(LogLevel.DEBUG, `"${bookMetadata.title}" Getting metadata with precedence [${metadataPrecedence.join(', ')}]`)
 | 
			
		||||
    for (const metadataSource of metadataPrecedence) {
 | 
			
		||||
      if (bookMetadataSourceHandler[metadataSource]) {
 | 
			
		||||
@ -649,6 +650,14 @@ class BookScanner {
 | 
			
		||||
      AudioFileScanner.setBookMetadataFromAudioMetaTags(bookTitle, this.audioFiles, this.bookMetadata, this.libraryScan)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Metadata from .nfo file
 | 
			
		||||
     */
 | 
			
		||||
    async nfoFile() {
 | 
			
		||||
      if (!this.libraryItemData.metadataNfoLibraryFile) return
 | 
			
		||||
      await NfoFileScanner.scanBookNfoFile(this.libraryItemData.metadataNfoLibraryFile, this.bookMetadata)
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Description from desc.txt and narrator from reader.txt
 | 
			
		||||
     */
 | 
			
		||||
 | 
			
		||||
@ -132,6 +132,11 @@ class LibraryItemScanData {
 | 
			
		||||
    return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.opf')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /** @type {LibraryItem.LibraryFileObject} */
 | 
			
		||||
  get metadataNfoLibraryFile() {
 | 
			
		||||
    return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.nfo')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * 
 | 
			
		||||
   * @param {LibraryItem} existingLibraryItem 
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								server/scanner/NfoFileScanner.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								server/scanner/NfoFileScanner.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
			
		||||
const { parseNfoMetadata } = require('../utils/parsers/parseNfoMetadata')
 | 
			
		||||
const { readTextFile } = require('../utils/fileUtils')
 | 
			
		||||
 | 
			
		||||
class NfoFileScanner {
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Parse metadata from .nfo file found in library scan and update bookMetadata
 | 
			
		||||
   * 
 | 
			
		||||
   * @param {import('../models/LibraryItem').LibraryFileObject} nfoLibraryFileObj 
 | 
			
		||||
   * @param {Object} bookMetadata 
 | 
			
		||||
   */
 | 
			
		||||
  async scanBookNfoFile(nfoLibraryFileObj, bookMetadata) {
 | 
			
		||||
    const nfoText = await readTextFile(nfoLibraryFileObj.metadata.path)
 | 
			
		||||
    const nfoMetadata = nfoText ? await parseNfoMetadata(nfoText) : null
 | 
			
		||||
    if (nfoMetadata) {
 | 
			
		||||
      for (const key in nfoMetadata) {
 | 
			
		||||
        if (key === 'tags') { // Add tags only if tags are empty
 | 
			
		||||
          if (nfoMetadata.tags.length) {
 | 
			
		||||
            bookMetadata.tags = nfoMetadata.tags
 | 
			
		||||
          }
 | 
			
		||||
        } else if (key === 'genres') { // Add genres only if genres are empty
 | 
			
		||||
          if (nfoMetadata.genres.length) {
 | 
			
		||||
            bookMetadata.genres = nfoMetadata.genres
 | 
			
		||||
          }
 | 
			
		||||
        } else if (key === 'authors') {
 | 
			
		||||
          if (nfoMetadata.authors?.length) {
 | 
			
		||||
            bookMetadata.authors = nfoMetadata.authors
 | 
			
		||||
          }
 | 
			
		||||
        } else if (key === 'narrators') {
 | 
			
		||||
          if (nfoMetadata.narrators?.length) {
 | 
			
		||||
            bookMetadata.narrators = nfoMetadata.narrators
 | 
			
		||||
          }
 | 
			
		||||
        } else if (key === 'series') {
 | 
			
		||||
          if (nfoMetadata.series) {
 | 
			
		||||
            bookMetadata.series = [{
 | 
			
		||||
              name: nfoMetadata.series,
 | 
			
		||||
              sequence: nfoMetadata.sequence || null
 | 
			
		||||
            }]
 | 
			
		||||
          }
 | 
			
		||||
        } else if (nfoMetadata[key] && key !== 'sequence') {
 | 
			
		||||
          bookMetadata[key] = nfoMetadata[key]
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
module.exports = new NfoFileScanner()
 | 
			
		||||
							
								
								
									
										94
									
								
								server/utils/parsers/parseNfoMetadata.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								server/utils/parsers/parseNfoMetadata.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,94 @@
 | 
			
		||||
function parseNfoMetadata(nfoText) {
 | 
			
		||||
  if (!nfoText) return null
 | 
			
		||||
  const lines = nfoText.split(/\r?\n/)
 | 
			
		||||
  const metadata = {}
 | 
			
		||||
  let insideBookDescription = false
 | 
			
		||||
  lines.forEach(line => {
 | 
			
		||||
    if (line.search(/^\s*book description\s*$/i) !== -1) {
 | 
			
		||||
      insideBookDescription = true
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (insideBookDescription) {
 | 
			
		||||
      if (line.search(/^\s*=+\s*$/i) !== -1) return
 | 
			
		||||
      metadata.description = metadata.description || ''
 | 
			
		||||
      metadata.description += line + '\n'
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    const match = line.match(/^(.*?):(.*)$/)
 | 
			
		||||
    if (match) {
 | 
			
		||||
      const key = match[1].toLowerCase().trim()
 | 
			
		||||
      const value = match[2].trim()
 | 
			
		||||
      if (!value) return
 | 
			
		||||
      switch (key) {
 | 
			
		||||
        case 'title':
 | 
			
		||||
          {
 | 
			
		||||
            const titleMatch = value.match(/^(.*?):(.*)$/)
 | 
			
		||||
            if (titleMatch) {
 | 
			
		||||
              metadata.title = titleMatch[1].trim()
 | 
			
		||||
              metadata.subtitle = titleMatch[2].trim()
 | 
			
		||||
            } else {
 | 
			
		||||
              metadata.title = value
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          break
 | 
			
		||||
        case 'author':
 | 
			
		||||
          metadata.authors = value.split(/\s*,\s*/)
 | 
			
		||||
          break
 | 
			
		||||
        case 'narrator':
 | 
			
		||||
        case 'read by':
 | 
			
		||||
          metadata.narrators = value.split(/\s*,\s*/)
 | 
			
		||||
          break
 | 
			
		||||
        case 'series name':
 | 
			
		||||
          metadata.series = value
 | 
			
		||||
          break
 | 
			
		||||
        case 'genre':
 | 
			
		||||
          metadata.genres = value.split(/\s*,\s*/)
 | 
			
		||||
          break
 | 
			
		||||
        case 'tags':
 | 
			
		||||
          metadata.tags = value.split(/\s*,\s*/)
 | 
			
		||||
          break
 | 
			
		||||
        case 'copyright':
 | 
			
		||||
        case 'audible.com release':
 | 
			
		||||
        case 'audiobook copyright':
 | 
			
		||||
        case 'book copyright':
 | 
			
		||||
        case 'recording copyright':
 | 
			
		||||
        case 'release date':
 | 
			
		||||
        case 'date':
 | 
			
		||||
          {
 | 
			
		||||
            const year = extractYear(value)
 | 
			
		||||
            if (year) {
 | 
			
		||||
              metadata.publishedYear = year
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case 'position in series':
 | 
			
		||||
          metadata.sequence = value
 | 
			
		||||
          break
 | 
			
		||||
        case 'unabridged':
 | 
			
		||||
          metadata.abridged = value.toLowerCase() === 'yes' ? false : true
 | 
			
		||||
          break
 | 
			
		||||
        case 'abridged':
 | 
			
		||||
          metadata.abridged = value.toLowerCase() === 'no' ? false : true
 | 
			
		||||
          break
 | 
			
		||||
        case 'publisher':
 | 
			
		||||
          metadata.publisher = value
 | 
			
		||||
          break
 | 
			
		||||
        case 'asin':
 | 
			
		||||
          metadata.asin = value
 | 
			
		||||
          break
 | 
			
		||||
        case 'isbn': 
 | 
			
		||||
        case 'isbn-10': 
 | 
			
		||||
        case 'isbn-13': 
 | 
			
		||||
          metadata.isbn = value
 | 
			
		||||
          break
 | 
			
		||||
      }  
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  return metadata
 | 
			
		||||
}
 | 
			
		||||
module.exports = { parseNfoMetadata }
 | 
			
		||||
 | 
			
		||||
function extractYear(str) {
 | 
			
		||||
  const match = str.match(/\d{4}/g)
 | 
			
		||||
  return match ? match[match.length-1] : null
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user