mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add:Experimental embed metadata in audio files #141
This commit is contained in:
		
							parent
							
								
									5f0f8b92d1
								
							
						
					
					
						commit
						84c12a6e7e
					
				@ -38,7 +38,9 @@
 | 
			
		||||
      </div>
 | 
			
		||||
      <draggable v-model="files" v-bind="dragOptions" class="list-group border border-gray-600" draggable=".item" tag="ul" @start="drag = true" @end="drag = false" @update="draggableUpdate">
 | 
			
		||||
        <transition-group type="transition" :name="!drag ? 'flip-list' : null">
 | 
			
		||||
          <li v-for="(audio, index) in files" :key="audio.ino" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center">
 | 
			
		||||
          <li v-for="(audio, index) in files" :key="audio.ino" :class="audio.include ? 'item' : 'exclude'" class="w-full list-group-item flex items-center relative">
 | 
			
		||||
            <div v-if="audiofilesEncoding[audio.ino]" class="absolute top-0 left-0 w-full h-full bg-success bg-opacity-25" />
 | 
			
		||||
 | 
			
		||||
            <div class="font-book text-center px-4 py-1 w-12">
 | 
			
		||||
              {{ audio.include ? index - numExcluded + 1 : -1 }}
 | 
			
		||||
            </div>
 | 
			
		||||
@ -71,12 +73,18 @@
 | 
			
		||||
            <div class="font-sans text-xs font-normal w-56">
 | 
			
		||||
              {{ audio.error }}
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="font-sans text-xs font-normal w-40 flex justify-center">
 | 
			
		||||
              <ui-toggle-switch v-model="audio.include" :off-color="'error'" @input="includeToggled(audio)" />
 | 
			
		||||
            <div class="font-sans text-xs font-normal w-40 flex items-center justify-center">
 | 
			
		||||
              <widgets-loading-spinner v-if="audiofilesEncoding[audio.ino]" />
 | 
			
		||||
              <p v-if="audiofilesEncoding[audio.ino]" class="text-warning pl-4 text-base">Encoding</p>
 | 
			
		||||
              <ui-toggle-switch v-else v-model="audio.include" :off-color="'error'" @input="includeToggled(audio)" />
 | 
			
		||||
            </div>
 | 
			
		||||
          </li>
 | 
			
		||||
        </transition-group>
 | 
			
		||||
      </draggable>
 | 
			
		||||
 | 
			
		||||
      <div v-if="showExperimentalFeatures && isRootUser" class="w-full flex justify-end items-center py-6">
 | 
			
		||||
        <ui-btn color="primary" small :loading="updatingMetadata" @click="updateAudioFileMetadata">Encode metadata in audio files <span class="text-warning font-bold text-xs">(experimental)</span></ui-btn>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@ -125,10 +133,18 @@ export default {
 | 
			
		||||
        ghostClass: 'ghost'
 | 
			
		||||
      },
 | 
			
		||||
      saving: false,
 | 
			
		||||
      currentSort: 'current'
 | 
			
		||||
      currentSort: 'current',
 | 
			
		||||
      updatingMetadata: false,
 | 
			
		||||
      audiofilesEncoding: {}
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    showExperimentalFeatures() {
 | 
			
		||||
      return this.$store.state.showExperimentalFeatures
 | 
			
		||||
    },
 | 
			
		||||
    isRootUser() {
 | 
			
		||||
      return this.$store.getters['user/getIsRoot']
 | 
			
		||||
    },
 | 
			
		||||
    media() {
 | 
			
		||||
      return this.libraryItem.media || {}
 | 
			
		||||
    },
 | 
			
		||||
@ -162,12 +178,23 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    streamLibraryItem() {
 | 
			
		||||
      return this.$store.state.streamLibraryItem
 | 
			
		||||
    },
 | 
			
		||||
    showExperimentalFeatures() {
 | 
			
		||||
      return this.$store.state.showExperimentalFeatures
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    updateAudioFileMetadata() {
 | 
			
		||||
      if (confirm(`Warning!\n\nThis will modify the audio files for this audiobook.\nMake sure your audio files are backed up before using this feature.`)) {
 | 
			
		||||
        this.updatingMetadata = true
 | 
			
		||||
        this.$axios
 | 
			
		||||
          .$get(`/api/items/${this.libraryItemId}/audio-metadata`)
 | 
			
		||||
          .then(() => {
 | 
			
		||||
            console.log('Audio metadata encode started')
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error('Audio metadata encode failed', error)
 | 
			
		||||
            this.updatingMetadata = false
 | 
			
		||||
          })
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    draggableUpdate(e) {
 | 
			
		||||
      this.currentSort = ''
 | 
			
		||||
    },
 | 
			
		||||
@ -242,7 +269,33 @@ export default {
 | 
			
		||||
      } else {
 | 
			
		||||
        return 'check_circle'
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    audioMetadataStarted(data) {
 | 
			
		||||
      console.log('audio metadata started', data)
 | 
			
		||||
      if (data.libraryItemId !== this.libraryItemId) return
 | 
			
		||||
      this.updatingMetadata = true
 | 
			
		||||
    },
 | 
			
		||||
    audioMetadataFinished(data) {
 | 
			
		||||
      console.log('audio metadata finished', data)
 | 
			
		||||
      if (data.libraryItemId !== this.libraryItemId) return
 | 
			
		||||
      this.updatingMetadata = false
 | 
			
		||||
      this.audiofilesEncoding = {}
 | 
			
		||||
      this.$toast.success('Audio file metadata updated')
 | 
			
		||||
    },
 | 
			
		||||
    audiofileMetadataStarted(data) {
 | 
			
		||||
      if (data.libraryItemId !== this.libraryItemId) return
 | 
			
		||||
      this.$set(this.audiofilesEncoding, data.ino, true)
 | 
			
		||||
    },
 | 
			
		||||
    audiofileMetadataFinished(data) {
 | 
			
		||||
      if (data.libraryItemId !== this.libraryItemId) return
 | 
			
		||||
      this.$set(this.audiofilesEncoding, data.ino, false)
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  mounted() {
 | 
			
		||||
    this.$root.socket.on('audio_metadata_started', this.audioMetadataStarted)
 | 
			
		||||
    this.$root.socket.on('audio_metadata_finished', this.audioMetadataFinished)
 | 
			
		||||
    this.$root.socket.on('audiofile_metadata_started', this.audiofileMetadataStarted)
 | 
			
		||||
    this.$root.socket.on('audiofile_metadata_finished', this.audiofileMetadataFinished)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ const LogManager = require('./managers/LogManager')
 | 
			
		||||
const BackupManager = require('./managers/BackupManager')
 | 
			
		||||
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
 | 
			
		||||
const PodcastManager = require('./managers/PodcastManager')
 | 
			
		||||
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  constructor(PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) {
 | 
			
		||||
@ -72,11 +73,12 @@ class Server {
 | 
			
		||||
    this.playbackSessionManager = new PlaybackSessionManager(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
			
		||||
    this.coverManager = new CoverManager(this.db, this.cacheManager)
 | 
			
		||||
    this.podcastManager = new PodcastManager(this.db, this.watcher, this.emitter.bind(this))
 | 
			
		||||
    this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
			
		||||
 | 
			
		||||
    this.scanner = new Scanner(this.db, this.coverManager, this.emitter.bind(this))
 | 
			
		||||
 | 
			
		||||
    // Routers
 | 
			
		||||
    this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
			
		||||
    this.apiRouter = new ApiRouter(this.db, this.auth, this.scanner, this.playbackSessionManager, this.abMergeManager, this.coverManager, this.backupManager, this.watcher, this.cacheManager, this.podcastManager, this.audioMetadataManager, this.emitter.bind(this), this.clientEmitter.bind(this))
 | 
			
		||||
    this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager, this.emitter.bind(this))
 | 
			
		||||
    this.staticRouter = new StaticRouter(this.db)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -354,6 +354,22 @@ class LibraryItemController {
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // POST: api/items/:id/audio-metadata
 | 
			
		||||
  async updateAudioFileMetadata(req, res) {
 | 
			
		||||
    if (!req.user.isRoot) {
 | 
			
		||||
      Logger.error(`[LibraryItemController] Non-root user attempted to update audio metadata`, req.user)
 | 
			
		||||
      return res.sendStatus(403)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (req.libraryItem.isMissing || !req.libraryItem.hasAudioFiles || !req.libraryItem.isBook) {
 | 
			
		||||
      Logger.error(`[LibraryItemController] Invalid library item`)
 | 
			
		||||
      return res.sendStatus(500)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.audioMetadataManager.updateAudioFileMetadataForItem(req.user, req.libraryItem)
 | 
			
		||||
    res.sendStatus(200)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  middleware(req, res, next) {
 | 
			
		||||
    var item = this.db.libraryItems.find(li => li.id === req.params.id)
 | 
			
		||||
    if (!item || !item.media) return res.sendStatus(404)
 | 
			
		||||
 | 
			
		||||
@ -121,7 +121,7 @@ class AbMergeManager {
 | 
			
		||||
        '-acodec aac',
 | 
			
		||||
        '-ac 2',
 | 
			
		||||
        '-b:a 64k',
 | 
			
		||||
        '-id3v2_version 3'
 | 
			
		||||
        '-movflags use_metadata_tags'
 | 
			
		||||
      ])
 | 
			
		||||
    } else {
 | 
			
		||||
      ffmpegOptions.push('-max_muxing_queue_size 1000')
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										137
									
								
								server/managers/AudioMetadataManager.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								server/managers/AudioMetadataManager.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,137 @@
 | 
			
		||||
const Path = require('path')
 | 
			
		||||
const fs = require('fs-extra')
 | 
			
		||||
const workerThreads = require('worker_threads')
 | 
			
		||||
const Logger = require('../Logger')
 | 
			
		||||
const filePerms = require('../utils/filePerms')
 | 
			
		||||
const { secondsToTimestamp } = require('../utils/index')
 | 
			
		||||
const { writeMetadataFile } = require('../utils/ffmpegHelpers')
 | 
			
		||||
 | 
			
		||||
class AudioMetadataMangaer {
 | 
			
		||||
  constructor(db, emitter, clientEmitter) {
 | 
			
		||||
    this.db = db
 | 
			
		||||
    this.emitter = emitter
 | 
			
		||||
    this.clientEmitter = clientEmitter
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async updateAudioFileMetadataForItem(user, libraryItem) {
 | 
			
		||||
    var audioFiles = libraryItem.media.audioFiles
 | 
			
		||||
 | 
			
		||||
    const itemAudioMetadataPayload = {
 | 
			
		||||
      userId: user.id,
 | 
			
		||||
      libraryItemId: libraryItem.id,
 | 
			
		||||
      startedAt: Date.now(),
 | 
			
		||||
      audioFiles: audioFiles.map(af => ({ index: af.index, ino: af.ino, filename: af.metadata.filename }))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.emitter('audio_metadata_started', itemAudioMetadataPayload)
 | 
			
		||||
 | 
			
		||||
    var downloadsPath = Path.join(global.MetadataPath, 'downloads')
 | 
			
		||||
    var outputDir = Path.join(downloadsPath, libraryItem.id)
 | 
			
		||||
    await fs.ensureDir(outputDir)
 | 
			
		||||
 | 
			
		||||
    var metadataFilePath = Path.join(outputDir, 'metadata.txt')
 | 
			
		||||
    await writeMetadataFile(libraryItem, metadataFilePath)
 | 
			
		||||
 | 
			
		||||
    const proms = audioFiles.map(af => {
 | 
			
		||||
      return this.updateAudioFileMetadata(libraryItem.id, af, outputDir, metadataFilePath)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    const results = await Promise.all(proms)
 | 
			
		||||
 | 
			
		||||
    Logger.debug(`[AudioMetadataManager] Finished`, results)
 | 
			
		||||
 | 
			
		||||
    await fs.remove(outputDir)
 | 
			
		||||
 | 
			
		||||
    const elapsed = Date.now() - itemAudioMetadataPayload.startedAt
 | 
			
		||||
    Logger.debug(`[AudioMetadataManager] Elapsed ${secondsToTimestamp(elapsed)}`)
 | 
			
		||||
    itemAudioMetadataPayload.results = results
 | 
			
		||||
    itemAudioMetadataPayload.elapsed = elapsed
 | 
			
		||||
    itemAudioMetadataPayload.finishedAt = Date.now()
 | 
			
		||||
    this.emitter('audio_metadata_finished', itemAudioMetadataPayload)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateAudioFileMetadata(libraryItemId, audioFile, outputDir, metadataFilePath) {
 | 
			
		||||
    return new Promise((resolve) => {
 | 
			
		||||
      const resultPayload = {
 | 
			
		||||
        libraryItemId,
 | 
			
		||||
        index: audioFile.index,
 | 
			
		||||
        ino: audioFile.ino,
 | 
			
		||||
        filename: audioFile.metadata.filename
 | 
			
		||||
      }
 | 
			
		||||
      this.emitter('audiofile_metadata_started', resultPayload)
 | 
			
		||||
 | 
			
		||||
      Logger.debug(`[AudioFileMetadataManager] Starting audio file metadata encode for "${audioFile.metadata.filename}"`)
 | 
			
		||||
 | 
			
		||||
      var outputPath = Path.join(outputDir, audioFile.metadata.filename)
 | 
			
		||||
      var inputPath = audioFile.metadata.path
 | 
			
		||||
      const isM4b = audioFile.metadata.format === 'm4b'
 | 
			
		||||
      const ffmpegInputs = [
 | 
			
		||||
        {
 | 
			
		||||
          input: inputPath,
 | 
			
		||||
          options: isM4b ? ['-f mp4'] : []
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          input: metadataFilePath
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
      /*
 | 
			
		||||
        Mp4 doesnt support writing custom tags by default. Supported tags are itunes tags: https://git.videolan.org/?p=ffmpeg.git;a=blob;f=libavformat/movenc.c;h=b6821d447c92183101086cb67099b2f4804293de;hb=HEAD#l2905
 | 
			
		||||
 | 
			
		||||
        Workaround -movflags use_metadata_tags found here: https://superuser.com/a/1208277      
 | 
			
		||||
        
 | 
			
		||||
        Ffmpeg premapped id3 tags: https://wiki.multimedia.cx/index.php/FFmpeg_Metadata
 | 
			
		||||
      */
 | 
			
		||||
 | 
			
		||||
      const ffmpegOptions = ['-c copy', '-map_metadata 1', `-metadata track=${audioFile.index}`, '-write_id3v2 1', '-movflags use_metadata_tags']
 | 
			
		||||
      var workerData = {
 | 
			
		||||
        inputs: ffmpegInputs,
 | 
			
		||||
        options: ffmpegOptions,
 | 
			
		||||
        outputOptions: isM4b ? ['-f mp4'] : [],
 | 
			
		||||
        output: outputPath,
 | 
			
		||||
      }
 | 
			
		||||
      var workerPath = Path.join(global.appRoot, 'server/utils/downloadWorker.js')
 | 
			
		||||
      var worker = new workerThreads.Worker(workerPath, { workerData })
 | 
			
		||||
 | 
			
		||||
      worker.on('message', async (message) => {
 | 
			
		||||
        if (message != null && typeof message === 'object') {
 | 
			
		||||
          if (message.type === 'RESULT') {
 | 
			
		||||
            Logger.debug(message)
 | 
			
		||||
 | 
			
		||||
            if (message.success) {
 | 
			
		||||
              Logger.debug(`[AudioFileMetadataManager] Metadata encode SUCCESS for "${audioFile.metadata.filename}"`)
 | 
			
		||||
 | 
			
		||||
              await filePerms.setDefault(outputPath, true)
 | 
			
		||||
 | 
			
		||||
              fs.move(outputPath, inputPath, { overwrite: true }).then(() => {
 | 
			
		||||
                Logger.debug(`[AudioFileMetadataManager] Audio file replaced successfully "${inputPath}"`)
 | 
			
		||||
 | 
			
		||||
                resultPayload.success = true
 | 
			
		||||
                this.emitter('audiofile_metadata_finished', resultPayload)
 | 
			
		||||
                resolve(resultPayload)
 | 
			
		||||
              }).catch((error) => {
 | 
			
		||||
                Logger.error(`[AudioFileMetadataManager] Audio file failed to move "${inputPath}"`, error)
 | 
			
		||||
                resultPayload.success = false
 | 
			
		||||
                this.emitter('audiofile_metadata_finished', resultPayload)
 | 
			
		||||
                resolve(resultPayload)
 | 
			
		||||
              })
 | 
			
		||||
            } else {
 | 
			
		||||
              Logger.debug(`[AudioFileMetadataManager] Metadata encode FAILED for "${audioFile.metadata.filename}"`)
 | 
			
		||||
 | 
			
		||||
              resultPayload.success = false
 | 
			
		||||
              this.emitter('audiofile_metadata_finished', resultPayload)
 | 
			
		||||
              resolve(resultPayload)
 | 
			
		||||
            }
 | 
			
		||||
          } else if (message.type === 'FFMPEG') {
 | 
			
		||||
            if (Logger[message.level]) {
 | 
			
		||||
              Logger[message.level](message.log)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          Logger.error('Invalid worker message', message)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
module.exports = AudioMetadataMangaer
 | 
			
		||||
@ -25,7 +25,7 @@ const Series = require('../objects/entities/Series')
 | 
			
		||||
const FileSystemController = require('../controllers/FileSystemController')
 | 
			
		||||
 | 
			
		||||
class ApiRouter {
 | 
			
		||||
  constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, emitter, clientEmitter) {
 | 
			
		||||
  constructor(db, auth, scanner, playbackSessionManager, abMergeManager, coverManager, backupManager, watcher, cacheManager, podcastManager, audioMetadataManager, emitter, clientEmitter) {
 | 
			
		||||
    this.db = db
 | 
			
		||||
    this.auth = auth
 | 
			
		||||
    this.scanner = scanner
 | 
			
		||||
@ -36,6 +36,7 @@ class ApiRouter {
 | 
			
		||||
    this.watcher = watcher
 | 
			
		||||
    this.cacheManager = cacheManager
 | 
			
		||||
    this.podcastManager = podcastManager
 | 
			
		||||
    this.audioMetadataManager = audioMetadataManager
 | 
			
		||||
    this.emitter = emitter
 | 
			
		||||
    this.clientEmitter = clientEmitter
 | 
			
		||||
 | 
			
		||||
@ -91,6 +92,7 @@ class ApiRouter {
 | 
			
		||||
    this.router.patch('/items/:id/episodes', LibraryItemController.middleware.bind(this), LibraryItemController.updateEpisodes.bind(this))
 | 
			
		||||
    this.router.delete('/items/:id/episode/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.removeEpisode.bind(this))
 | 
			
		||||
    this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) // Root only
 | 
			
		||||
    this.router.get('/items/:id/audio-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.updateAudioFileMetadata.bind(this)) // Root only
 | 
			
		||||
 | 
			
		||||
    this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
 | 
			
		||||
    this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
 | 
			
		||||
 | 
			
		||||
@ -39,7 +39,7 @@ async function runFfmpeg() {
 | 
			
		||||
    ffmpegCommand.on('stderr', (stdErrline) => {
 | 
			
		||||
      parentPort.postMessage({
 | 
			
		||||
        type: 'FFMPEG',
 | 
			
		||||
        level: 'error',
 | 
			
		||||
        level: 'debug',
 | 
			
		||||
        log: '[DownloadWorker] Ffmpeg Stderr: ' + stdErrline
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
@ -48,10 +48,33 @@ async function writeMetadataFile(libraryItem, outputPath) {
 | 
			
		||||
    `artist=${libraryItem.media.metadata.authorName}`,
 | 
			
		||||
    `album_artist=${libraryItem.media.metadata.authorName}`,
 | 
			
		||||
    `date=${libraryItem.media.metadata.publishedYear || ''}`,
 | 
			
		||||
    `description=${libraryItem.media.metadata.description}`,
 | 
			
		||||
    `genre=${libraryItem.media.metadata.genres.join(';')}`
 | 
			
		||||
    `description=${libraryItem.media.metadata.description || ''}`,
 | 
			
		||||
    `genre=${libraryItem.media.metadata.genres.join(';')}`,
 | 
			
		||||
    `performer=${libraryItem.media.metadata.narratorName || ''}`,
 | 
			
		||||
    `encoded_by=audiobookshelf:${package.version}`
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  if (libraryItem.media.metadata.asin) {
 | 
			
		||||
    inputstrs.push(`ASIN=${libraryItem.media.metadata.asin}`)
 | 
			
		||||
  }
 | 
			
		||||
  if (libraryItem.media.metadata.isbn) {
 | 
			
		||||
    inputstrs.push(`ISBN=${libraryItem.media.metadata.isbn}`)
 | 
			
		||||
  }
 | 
			
		||||
  if (libraryItem.media.metadata.language) {
 | 
			
		||||
    inputstrs.push(`language=${libraryItem.media.metadata.language}`)
 | 
			
		||||
  }
 | 
			
		||||
  if (libraryItem.media.metadata.series.length) {
 | 
			
		||||
    // Only uses first series
 | 
			
		||||
    var firstSeries = libraryItem.media.metadata.series[0]
 | 
			
		||||
    inputstrs.push(`series=${firstSeries.name}`)
 | 
			
		||||
    if (firstSeries.sequence) {
 | 
			
		||||
      inputstrs.push(`series-part=${firstSeries.sequence}`)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (libraryItem.media.metadata.subtitle) {
 | 
			
		||||
    inputstrs.push(`subtitle=${libraryItem.media.metadata.subtitle}`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (libraryItem.media.chapters) {
 | 
			
		||||
    libraryItem.media.chapters.forEach((chap) => {
 | 
			
		||||
      const chapterstrs = [
 | 
			
		||||
 | 
			
		||||
@ -204,12 +204,12 @@ function parseTags(format, verbose) {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var keysToLookOutFor = ['file_tag_genre1', 'file_tag_genre2', 'file_tag_series', 'file_tag_seriespart', 'file_tag_movement', 'file_tag_movementname', 'file_tag_wwwaudiofile', 'file_tag_contentgroup', 'file_tag_releasetime', 'file_tag_isbn']
 | 
			
		||||
  keysToLookOutFor.forEach((key) => {
 | 
			
		||||
    if (tags[key]) {
 | 
			
		||||
      Logger.debug(`Notable! ${key} => ${tags[key]}`)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  // var keysToLookOutFor = ['file_tag_genre1', 'file_tag_genre2', 'file_tag_series', 'file_tag_seriespart', 'file_tag_movement', 'file_tag_movementname', 'file_tag_wwwaudiofile', 'file_tag_contentgroup', 'file_tag_releasetime', 'file_tag_isbn']
 | 
			
		||||
  // keysToLookOutFor.forEach((key) => {
 | 
			
		||||
  //   if (tags[key]) {
 | 
			
		||||
  //     Logger.debug(`Notable! ${key} => ${tags[key]}`)
 | 
			
		||||
  //   }
 | 
			
		||||
  // })
 | 
			
		||||
  return tags
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user