mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Update:Experimental metadata embed tool to use tone
This commit is contained in:
		
							parent
							
								
									b6e3559aba
								
							
						
					
					
						commit
						97da73baf3
					
				@ -1,29 +1,31 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div id="page-wrapper" class="bg-bg page p-8 overflow-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
 | 
					  <div id="page-wrapper" class="bg-bg page p-8 overflow-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
 | 
				
			||||||
    <div class="flex justify-center mb-2">
 | 
					    <div class="flex justify-center mb-4">
 | 
				
			||||||
      <div class="w-full max-w-2xl">
 | 
					      <div class="w-full max-w-2xl">
 | 
				
			||||||
        <p class="text-xl">Metadata to embed</p>
 | 
					        <p class="text-xl mb-2">Metadata to embed</p>
 | 
				
			||||||
 | 
					        <p class="mb-4 text-base text-gray-300">audiobookshelf uses <a href="https://github.com/sandreas/tone" target="_blank" class="hover:underline text-blue-400 hover:text-blue-300">tone</a> to write metadata.</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="w-full max-w-2xl"></div>
 | 
					      <div class="w-full max-w-2xl"></div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="flex justify-center flex-wrap">
 | 
					    <div class="flex justify-center flex-wrap">
 | 
				
			||||||
      <div class="w-full max-w-2xl border border-opacity-10 bg-bg mx-2">
 | 
					      <div class="w-full max-w-2xl border border-white border-opacity-10 bg-bg mx-2">
 | 
				
			||||||
        <div class="flex py-2 px-4">
 | 
					        <div class="flex py-2 px-4">
 | 
				
			||||||
          <div class="w-1/3 text-xs font-semibold uppercase text-gray-200">Meta Tag</div>
 | 
					          <div class="w-1/3 text-xs font-semibold uppercase text-gray-200">Meta Tag</div>
 | 
				
			||||||
          <div class="w-2/3 text-xs font-semibold uppercase text-gray-200">Value</div>
 | 
					          <div class="w-2/3 text-xs font-semibold uppercase text-gray-200">Value</div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="w-full max-h-72 overflow-auto">
 | 
					        <div class="w-full max-h-72 overflow-auto">
 | 
				
			||||||
          <template v-for="(keyValue, index) in metadataKeyValues">
 | 
					          <template v-for="(value, key, index) in toneObject">
 | 
				
			||||||
            <div :key="keyValue.key" class="flex py-1 px-4 text-sm" :class="index % 2 === 0 ? 'bg-primary bg-opacity-25' : ''">
 | 
					            <div :key="key" class="flex py-1 px-4 text-sm" :class="index % 2 === 0 ? 'bg-primary bg-opacity-25' : ''">
 | 
				
			||||||
              <div class="w-1/3 font-semibold">{{ keyValue.key }}</div>
 | 
					              <div class="w-1/3 font-semibold">{{ key }}</div>
 | 
				
			||||||
              <div class="w-2/3">
 | 
					              <div class="w-2/3">
 | 
				
			||||||
                {{ keyValue.value }}
 | 
					                {{ value }}
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </template>
 | 
					          </template>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="w-full max-w-2xl border border-opacity-10 bg-bg mx-2">
 | 
					      <div class="w-full max-w-2xl border border-white border-opacity-10 bg-bg mx-2">
 | 
				
			||||||
        <div class="flex py-2 px-4">
 | 
					        <div class="flex py-2 px-4">
 | 
				
			||||||
          <div class="flex-grow text-xs font-semibold uppercase text-gray-200">Chapter Title</div>
 | 
					          <div class="flex-grow text-xs font-semibold uppercase text-gray-200">Chapter Title</div>
 | 
				
			||||||
          <div class="w-24 text-xs font-semibold uppercase text-gray-200">Start</div>
 | 
					          <div class="w-24 text-xs font-semibold uppercase text-gray-200">Start</div>
 | 
				
			||||||
@ -34,10 +36,10 @@
 | 
				
			|||||||
            <div :key="index" class="flex py-1 px-4 text-sm" :class="index % 2 === 0 ? 'bg-primary bg-opacity-25' : ''">
 | 
					            <div :key="index" class="flex py-1 px-4 text-sm" :class="index % 2 === 0 ? 'bg-primary bg-opacity-25' : ''">
 | 
				
			||||||
              <div class="flex-grow font-semibold">{{ chapter.title }}</div>
 | 
					              <div class="flex-grow font-semibold">{{ chapter.title }}</div>
 | 
				
			||||||
              <div class="w-24">
 | 
					              <div class="w-24">
 | 
				
			||||||
                {{ chapter.start.toFixed(2) }}
 | 
					                {{ $secondsToTimestamp(chapter.start) }}
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="w-24">
 | 
					              <div class="w-24">
 | 
				
			||||||
                {{ chapter.end.toFixed(2) }}
 | 
					                {{ $secondsToTimestamp(chapter.end) }}
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </template>
 | 
					          </template>
 | 
				
			||||||
@ -48,12 +50,17 @@
 | 
				
			|||||||
    <div class="w-full h-px bg-white bg-opacity-10 my-8" />
 | 
					    <div class="w-full h-px bg-white bg-opacity-10 my-8" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="w-full max-w-4xl mx-auto">
 | 
					    <div class="w-full max-w-4xl mx-auto">
 | 
				
			||||||
      <div class="w-full flex justify-between items-center mb-4">
 | 
					      <div class="w-full flex justify-between items-center mb-2">
 | 
				
			||||||
        <p class="text-warning text-lg font-semibold">Warning: Modifies your audio files</p>
 | 
					        <p class="text-warning text-lg font-semibold">Warning: Modifies your audio files</p>
 | 
				
			||||||
        <ui-btn v-if="!embedFinished" color="primary" :loading="updatingMetadata" @click="updateAudioFileMetadata">Embed Metadata</ui-btn>
 | 
					        <ui-btn v-if="!embedFinished" color="primary" :loading="updatingMetadata" @click.stop="embedClick">Embed Metadata</ui-btn>
 | 
				
			||||||
        <p v-else class="text-success text-lg font-semibold">Embed Finished!</p>
 | 
					        <p v-else class="text-success text-lg font-semibold">Embed Finished!</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="w-full mx-auto border border-opacity-10 bg-bg">
 | 
					      <div class="flex mb-4">
 | 
				
			||||||
 | 
					        <p class="text-gray-200">
 | 
				
			||||||
 | 
					          A backup of your audio files will be stored in <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span> so you can restore the originals if necessary.
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="w-full mx-auto border border-white border-opacity-10 bg-bg">
 | 
				
			||||||
        <div class="flex py-2 px-4">
 | 
					        <div class="flex py-2 px-4">
 | 
				
			||||||
          <div class="w-10 text-xs font-semibold text-gray-200">#</div>
 | 
					          <div class="w-10 text-xs font-semibold text-gray-200">#</div>
 | 
				
			||||||
          <div class="flex-grow text-xs font-semibold uppercase text-gray-200">Filename</div>
 | 
					          <div class="flex-grow text-xs font-semibold uppercase text-gray-200">Filename</div>
 | 
				
			||||||
@ -118,7 +125,8 @@ export default {
 | 
				
			|||||||
      audiofilesEncoding: {},
 | 
					      audiofilesEncoding: {},
 | 
				
			||||||
      audiofilesFinished: {},
 | 
					      audiofilesFinished: {},
 | 
				
			||||||
      updatingMetadata: false,
 | 
					      updatingMetadata: false,
 | 
				
			||||||
      embedFinished: false
 | 
					      embedFinished: false,
 | 
				
			||||||
 | 
					      toneObject: null
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
@ -137,98 +145,35 @@ export default {
 | 
				
			|||||||
    streamLibraryItem() {
 | 
					    streamLibraryItem() {
 | 
				
			||||||
      return this.$store.state.streamLibraryItem
 | 
					      return this.$store.state.streamLibraryItem
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    metadataKeyValues() {
 | 
					 | 
				
			||||||
      const keyValues = [
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          key: 'title',
 | 
					 | 
				
			||||||
          value: this.mediaMetadata.title
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          key: 'artist',
 | 
					 | 
				
			||||||
          value: this.mediaMetadata.authorName
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          key: 'album_artist',
 | 
					 | 
				
			||||||
          value: this.mediaMetadata.authorName
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          key: 'date',
 | 
					 | 
				
			||||||
          value: this.mediaMetadata.publishedYear
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          key: 'description',
 | 
					 | 
				
			||||||
          value: this.mediaMetadata.description
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          key: 'genre',
 | 
					 | 
				
			||||||
          value: this.mediaMetadata.genres.join(';')
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          key: 'performer',
 | 
					 | 
				
			||||||
          value: this.mediaMetadata.narratorName
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.mediaMetadata.subtitle) {
 | 
					 | 
				
			||||||
        keyValues.push({
 | 
					 | 
				
			||||||
          key: 'subtitle',
 | 
					 | 
				
			||||||
          value: this.mediaMetadata.subtitle
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.mediaMetadata.asin) {
 | 
					 | 
				
			||||||
        keyValues.push({
 | 
					 | 
				
			||||||
          key: 'asin',
 | 
					 | 
				
			||||||
          value: this.mediaMetadata.asin
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (this.mediaMetadata.isbn) {
 | 
					 | 
				
			||||||
        keyValues.push({
 | 
					 | 
				
			||||||
          key: 'isbn',
 | 
					 | 
				
			||||||
          value: this.mediaMetadata.isbn
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (this.mediaMetadata.language) {
 | 
					 | 
				
			||||||
        keyValues.push({
 | 
					 | 
				
			||||||
          key: 'language',
 | 
					 | 
				
			||||||
          value: this.mediaMetadata.language
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      if (this.mediaMetadata.series.length) {
 | 
					 | 
				
			||||||
        var firstSeries = this.mediaMetadata.series[0]
 | 
					 | 
				
			||||||
        keyValues.push({
 | 
					 | 
				
			||||||
          key: 'series',
 | 
					 | 
				
			||||||
          value: firstSeries.name
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        if (firstSeries.sequence) {
 | 
					 | 
				
			||||||
          keyValues.push({
 | 
					 | 
				
			||||||
            key: 'series-part',
 | 
					 | 
				
			||||||
            value: firstSeries.sequence
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return keyValues
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    metadataChapters() {
 | 
					    metadataChapters() {
 | 
				
			||||||
      var chapters = this.media.chapters || []
 | 
					      var chapters = this.media.chapters || []
 | 
				
			||||||
      return chapters.concat(chapters)
 | 
					      return chapters.concat(chapters)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    updateAudioFileMetadata() {
 | 
					    embedClick() {
 | 
				
			||||||
      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.`)) {
 | 
					      const payload = {
 | 
				
			||||||
        this.updatingMetadata = true
 | 
					        message: `Are you sure you want to embed metadata in ${this.audioFiles.length} audio files?`,
 | 
				
			||||||
        this.$axios
 | 
					        callback: (confirmed) => {
 | 
				
			||||||
          .$get(`/api/items/${this.libraryItemId}/audio-metadata`)
 | 
					          if (confirmed) {
 | 
				
			||||||
          .then(() => {
 | 
					            this.updateAudioFileMetadata()
 | 
				
			||||||
            console.log('Audio metadata encode started')
 | 
					          }
 | 
				
			||||||
          })
 | 
					        },
 | 
				
			||||||
          .catch((error) => {
 | 
					        type: 'yesNo'
 | 
				
			||||||
            console.error('Audio metadata encode failed', error)
 | 
					 | 
				
			||||||
            this.updatingMetadata = false
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      this.$store.commit('globals/setConfirmPrompt', payload)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    updateAudioFileMetadata() {
 | 
				
			||||||
 | 
					      this.updatingMetadata = true
 | 
				
			||||||
 | 
					      this.$axios
 | 
				
			||||||
 | 
					        .$get(`/api/items/${this.libraryItemId}/audio-metadata?tone=1`)
 | 
				
			||||||
 | 
					        .then(() => {
 | 
				
			||||||
 | 
					          console.log('Audio metadata encode started')
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch((error) => {
 | 
				
			||||||
 | 
					          console.error('Audio metadata encode failed', error)
 | 
				
			||||||
 | 
					          this.updatingMetadata = false
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    audioMetadataStarted(data) {
 | 
					    audioMetadataStarted(data) {
 | 
				
			||||||
      console.log('audio metadata started', data)
 | 
					      console.log('audio metadata started', data)
 | 
				
			||||||
@ -252,9 +197,21 @@ export default {
 | 
				
			|||||||
      if (data.libraryItemId !== this.libraryItemId) return
 | 
					      if (data.libraryItemId !== this.libraryItemId) return
 | 
				
			||||||
      this.$set(this.audiofilesEncoding, data.ino, false)
 | 
					      this.$set(this.audiofilesEncoding, data.ino, false)
 | 
				
			||||||
      this.$set(this.audiofilesFinished, data.ino, true)
 | 
					      this.$set(this.audiofilesFinished, data.ino, true)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    fetchToneObject() {
 | 
				
			||||||
 | 
					      this.$axios
 | 
				
			||||||
 | 
					        .$get(`/api/items/${this.libraryItemId}/tone-object`)
 | 
				
			||||||
 | 
					        .then((toneObject) => {
 | 
				
			||||||
 | 
					          delete toneObject.CoverFile
 | 
				
			||||||
 | 
					          this.toneObject = toneObject
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .catch((error) => {
 | 
				
			||||||
 | 
					          console.error('Failed to fetch tone object', error)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mounted() {
 | 
					  mounted() {
 | 
				
			||||||
 | 
					    this.fetchToneObject()
 | 
				
			||||||
    this.$root.socket.on('audio_metadata_started', this.audioMetadataStarted)
 | 
					    this.$root.socket.on('audio_metadata_started', this.audioMetadataStarted)
 | 
				
			||||||
    this.$root.socket.on('audio_metadata_finished', this.audioMetadataFinished)
 | 
					    this.$root.socket.on('audio_metadata_finished', this.audioMetadataFinished)
 | 
				
			||||||
    this.$root.socket.on('audiofile_metadata_started', this.audiofileMetadataStarted)
 | 
					    this.$root.socket.on('audiofile_metadata_started', this.audiofileMetadataStarted)
 | 
				
			||||||
 | 
				
			|||||||
@ -324,13 +324,13 @@ class LibraryItemController {
 | 
				
			|||||||
    res.sendStatus(200)
 | 
					    res.sendStatus(200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (let i = 0; i < items.length; i++) {
 | 
					    for (let i = 0; i < items.length; i++) {
 | 
				
			||||||
        var libraryItem = this.db.libraryItems.find(_li => _li.id === items[i])
 | 
					      var libraryItem = this.db.libraryItems.find(_li => _li.id === items[i])
 | 
				
			||||||
        var matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options)
 | 
					      var matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options)
 | 
				
			||||||
        if (matchResult.updated) {
 | 
					      if (matchResult.updated) {
 | 
				
			||||||
            itemsUpdated++
 | 
					        itemsUpdated++
 | 
				
			||||||
        } else if (matchResult.warning) {
 | 
					      } else if (matchResult.warning) {
 | 
				
			||||||
            itemsUnmatched++
 | 
					        itemsUnmatched++
 | 
				
			||||||
        }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var result = {
 | 
					    var result = {
 | 
				
			||||||
@ -371,6 +371,20 @@ class LibraryItemController {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getToneMetadataObject(req, res) {
 | 
				
			||||||
 | 
					    if (!req.user.isAdminOrUp) {
 | 
				
			||||||
 | 
					      Logger.error(`[LibraryItemController] Non-root user attempted to get tone metadata object`, 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)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.json(this.audioMetadataManager.getToneMetadataObjectForApi(req.libraryItem))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // GET: api/items/:id/audio-metadata
 | 
					  // GET: api/items/:id/audio-metadata
 | 
				
			||||||
  async updateAudioFileMetadata(req, res) {
 | 
					  async updateAudioFileMetadata(req, res) {
 | 
				
			||||||
    if (!req.user.isAdminOrUp) {
 | 
					    if (!req.user.isAdminOrUp) {
 | 
				
			||||||
@ -383,7 +397,8 @@ class LibraryItemController {
 | 
				
			|||||||
      return res.sendStatus(500)
 | 
					      return res.sendStatus(500)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.audioMetadataManager.updateAudioFileMetadataForItem(req.user, req.libraryItem)
 | 
					    const useTone = req.query.tone === '1'
 | 
				
			||||||
 | 
					    this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, useTone)
 | 
				
			||||||
    res.sendStatus(200)
 | 
					    res.sendStatus(200)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ const Logger = require('../Logger')
 | 
				
			|||||||
const filePerms = require('../utils/filePerms')
 | 
					const filePerms = require('../utils/filePerms')
 | 
				
			||||||
const { secondsToTimestamp } = require('../utils/index')
 | 
					const { secondsToTimestamp } = require('../utils/index')
 | 
				
			||||||
const { writeMetadataFile } = require('../utils/ffmpegHelpers')
 | 
					const { writeMetadataFile } = require('../utils/ffmpegHelpers')
 | 
				
			||||||
 | 
					const toneHelpers = require('../utils/toneHelpers')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AudioMetadataMangaer {
 | 
					class AudioMetadataMangaer {
 | 
				
			||||||
  constructor(db, emitter, clientEmitter) {
 | 
					  constructor(db, emitter, clientEmitter) {
 | 
				
			||||||
@ -13,7 +14,104 @@ class AudioMetadataMangaer {
 | 
				
			|||||||
    this.clientEmitter = clientEmitter
 | 
					    this.clientEmitter = clientEmitter
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async updateAudioFileMetadataForItem(user, libraryItem) {
 | 
					  updateMetadataForItem(user, libraryItem, useTone = true) {
 | 
				
			||||||
 | 
					    if (useTone) {
 | 
				
			||||||
 | 
					      this.updateMetadataForItemWithTone(user, libraryItem)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      this.updateMetadataForItemWithFfmpeg(user, libraryItem)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // TONE
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  getToneMetadataObjectForApi(libraryItem) {
 | 
				
			||||||
 | 
					    return toneHelpers.getToneMetadataObject(libraryItem)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async updateMetadataForItemWithTone(user, libraryItem) {
 | 
				
			||||||
 | 
					    var audioFiles = libraryItem.media.includedAudioFiles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Write chapters file
 | 
				
			||||||
 | 
					    var chaptersFilePath = null
 | 
				
			||||||
 | 
					    var cachePath = Path.join(global.MetadataPath, 'cache/items')
 | 
				
			||||||
 | 
					    console.log('Items Cache Path', cachePath)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var itemCacheDir = Path.join(cachePath, libraryItem.id)
 | 
				
			||||||
 | 
					    await fs.ensureDir(itemCacheDir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (libraryItem.media.chapters.length) {
 | 
				
			||||||
 | 
					      chaptersFilePath = Path.join(itemCacheDir, 'chapters.txt')
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        await toneHelpers.writeToneChaptersFile(libraryItem.media.chapters, chaptersFilePath)
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        Logger.error(`[AudioMetadataManager] Write chapters.txt failed`, error)
 | 
				
			||||||
 | 
					        chaptersFilePath = null
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const toneMetadataObject = toneHelpers.getToneMetadataObject(libraryItem, chaptersFilePath)
 | 
				
			||||||
 | 
					    Logger.debug(`[AudioMetadataManager] Book "${libraryItem.media.metadata.title}" tone metadata object=`, toneMetadataObject)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const results = []
 | 
				
			||||||
 | 
					    for (const af of audioFiles) {
 | 
				
			||||||
 | 
					      const result = await this.updateAudioFileMetadataWithTone(libraryItem.id, af, toneMetadataObject, itemCacheDir)
 | 
				
			||||||
 | 
					      results.push(result)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async updateAudioFileMetadataWithTone(libraryItemId, audioFile, toneMetadataObject, itemCacheDir) {
 | 
				
			||||||
 | 
					    const resultPayload = {
 | 
				
			||||||
 | 
					      libraryItemId,
 | 
				
			||||||
 | 
					      index: audioFile.index,
 | 
				
			||||||
 | 
					      ino: audioFile.ino,
 | 
				
			||||||
 | 
					      filename: audioFile.metadata.filename
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.emitter('audiofile_metadata_started', resultPayload)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Backup audio file
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const backupFilePath = Path.join(itemCacheDir, audioFile.metadata.filename)
 | 
				
			||||||
 | 
					      await fs.copy(audioFile.metadata.path, backupFilePath)
 | 
				
			||||||
 | 
					      Logger.debug(`[AudioMetadataManager] Backed up audio file at "${backupFilePath}"`)
 | 
				
			||||||
 | 
					    } catch (err) {
 | 
				
			||||||
 | 
					      Logger.error(`[AudioMetadataManager] Failed to backup audio file "${audioFile.metadata.path}"`, err)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const _toneMetadataObject = {
 | 
				
			||||||
 | 
					      ...toneMetadataObject,
 | 
				
			||||||
 | 
					      'TrackNumber': audioFile.index
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    resultPayload.success = await toneHelpers.tagAudioFile(audioFile.metadata.path, _toneMetadataObject)
 | 
				
			||||||
 | 
					    if (resultPayload.success) {
 | 
				
			||||||
 | 
					      Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${audioFile.metadata.path}"`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.emitter('audiofile_metadata_finished', resultPayload)
 | 
				
			||||||
 | 
					    return resultPayload
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  // FFMPEG
 | 
				
			||||||
 | 
					  //
 | 
				
			||||||
 | 
					  async updateMetadataForItemWithFfmpeg(user, libraryItem) {
 | 
				
			||||||
    var audioFiles = libraryItem.media.audioFiles
 | 
					    var audioFiles = libraryItem.media.audioFiles
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const itemAudioMetadataPayload = {
 | 
					    const itemAudioMetadataPayload = {
 | 
				
			||||||
@ -36,9 +134,8 @@ class AudioMetadataMangaer {
 | 
				
			|||||||
      var coverPath = libraryItem.media.coverPath.replace(/\\/g, '/')
 | 
					      var coverPath = libraryItem.media.coverPath.replace(/\\/g, '/')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // TODO: Split into batches
 | 
					 | 
				
			||||||
    const proms = audioFiles.map(af => {
 | 
					    const proms = audioFiles.map(af => {
 | 
				
			||||||
      return this.updateAudioFileMetadata(libraryItem.id, af, outputDir, metadataFilePath, coverPath)
 | 
					      return this.updateAudioFileMetadataWithFfmpeg(libraryItem.id, af, outputDir, metadataFilePath, coverPath)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const results = await Promise.all(proms)
 | 
					    const results = await Promise.all(proms)
 | 
				
			||||||
@ -55,7 +152,7 @@ class AudioMetadataMangaer {
 | 
				
			|||||||
    this.emitter('audio_metadata_finished', itemAudioMetadataPayload)
 | 
					    this.emitter('audio_metadata_finished', itemAudioMetadataPayload)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateAudioFileMetadata(libraryItemId, audioFile, outputDir, metadataFilePath, coverPath = '') {
 | 
					  updateAudioFileMetadataWithFfmpeg(libraryItemId, audioFile, outputDir, metadataFilePath, coverPath = '') {
 | 
				
			||||||
    return new Promise((resolve) => {
 | 
					    return new Promise((resolve) => {
 | 
				
			||||||
      const resultPayload = {
 | 
					      const resultPayload = {
 | 
				
			||||||
        libraryItemId,
 | 
					        libraryItemId,
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ class CacheManager {
 | 
				
			|||||||
    this.CachePath = Path.join(global.MetadataPath, 'cache')
 | 
					    this.CachePath = Path.join(global.MetadataPath, 'cache')
 | 
				
			||||||
    this.CoverCachePath = Path.join(this.CachePath, 'covers')
 | 
					    this.CoverCachePath = Path.join(this.CachePath, 'covers')
 | 
				
			||||||
    this.ImageCachePath = Path.join(this.CachePath, 'images')
 | 
					    this.ImageCachePath = Path.join(this.CachePath, 'images')
 | 
				
			||||||
 | 
					    this.ItemCachePath = Path.join(this.CachePath, 'items')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async ensureCachePaths() { // Creates cache paths if necessary and sets owner and permissions
 | 
					  async ensureCachePaths() { // Creates cache paths if necessary and sets owner and permissions
 | 
				
			||||||
@ -29,6 +30,11 @@ class CacheManager {
 | 
				
			|||||||
      pathsCreated = true
 | 
					      pathsCreated = true
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!(await fs.pathExists(this.ItemCachePath))) {
 | 
				
			||||||
 | 
					      await fs.mkdir(this.ItemCachePath)
 | 
				
			||||||
 | 
					      pathsCreated = true
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (pathsCreated) {
 | 
					    if (pathsCreated) {
 | 
				
			||||||
      await filePerms.setDefault(this.CachePath)
 | 
					      await filePerms.setDefault(this.CachePath)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,7 @@ const Logger = require('../../Logger')
 | 
				
			|||||||
const BookMetadata = require('../metadata/BookMetadata')
 | 
					const BookMetadata = require('../metadata/BookMetadata')
 | 
				
			||||||
const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index')
 | 
					const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index')
 | 
				
			||||||
const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata')
 | 
					const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata')
 | 
				
			||||||
const { overdriveMediaMarkersExist, parseOverdriveMediaMarkersAsChapters } = require('../../utils/parsers/parseOverdriveMediaMarkers')
 | 
					const { parseOverdriveMediaMarkersAsChapters } = require('../../utils/parsers/parseOverdriveMediaMarkers')
 | 
				
			||||||
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
 | 
					const abmetadataGenerator = require('../../utils/abmetadataGenerator')
 | 
				
			||||||
const { readTextFile } = require('../../utils/fileUtils')
 | 
					const { readTextFile } = require('../../utils/fileUtils')
 | 
				
			||||||
const AudioFile = require('../files/AudioFile')
 | 
					const AudioFile = require('../files/AudioFile')
 | 
				
			||||||
@ -111,12 +111,15 @@ class Book {
 | 
				
			|||||||
  get invalidAudioFiles() {
 | 
					  get invalidAudioFiles() {
 | 
				
			||||||
    return this.audioFiles.filter(af => af.invalid)
 | 
					    return this.audioFiles.filter(af => af.invalid)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  get includedAudioFiles() {
 | 
				
			||||||
 | 
					    return this.audioFiles.filter(af => !af.exclude && !af.invalid)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  get hasIssues() {
 | 
					  get hasIssues() {
 | 
				
			||||||
    return this.missingParts.length || this.invalidAudioFiles.length
 | 
					    return this.missingParts.length || this.invalidAudioFiles.length
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  get tracks() {
 | 
					  get tracks() {
 | 
				
			||||||
    var startOffset = 0
 | 
					    var startOffset = 0
 | 
				
			||||||
    return this.audioFiles.filter(af => !af.exclude && !af.invalid).map((af) => {
 | 
					    return this.includedAudioFiles.map((af) => {
 | 
				
			||||||
      var audioTrack = new AudioTrack()
 | 
					      var audioTrack = new AudioTrack()
 | 
				
			||||||
      audioTrack.setData(this.libraryItemId, af, startOffset)
 | 
					      audioTrack.setData(this.libraryItemId, af, startOffset)
 | 
				
			||||||
      startOffset += audioTrack.duration
 | 
					      startOffset += audioTrack.duration
 | 
				
			||||||
 | 
				
			|||||||
@ -133,6 +133,14 @@ class BookMetadata {
 | 
				
			|||||||
      return `${getTitleIgnorePrefix(se.name)} #${se.sequence}`
 | 
					      return `${getTitleIgnorePrefix(se.name)} #${se.sequence}`
 | 
				
			||||||
    }).join(', ')
 | 
					    }).join(', ')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  get firstSeriesName() {
 | 
				
			||||||
 | 
					    if (!this.series.length) return ''
 | 
				
			||||||
 | 
					    return this.series[0].name
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  get firstSeriesSequence() {
 | 
				
			||||||
 | 
					    if (!this.series.length) return ''
 | 
				
			||||||
 | 
					    return this.series[0].sequence
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  get narratorName() {
 | 
					  get narratorName() {
 | 
				
			||||||
    return this.narrators.join(', ')
 | 
					    return this.narrators.join(', ')
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -95,6 +95,7 @@ class ApiRouter {
 | 
				
			|||||||
    this.router.post('/items/:id/play/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.startEpisodePlaybackSession.bind(this))
 | 
					    this.router.post('/items/:id/play/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.startEpisodePlaybackSession.bind(this))
 | 
				
			||||||
    this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this))
 | 
					    this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this))
 | 
				
			||||||
    this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this))
 | 
					    this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this))
 | 
				
			||||||
 | 
					    this.router.get('/items/:id/tone-object', LibraryItemController.middleware.bind(this), LibraryItemController.getToneMetadataObject.bind(this))
 | 
				
			||||||
    this.router.get('/items/:id/audio-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.updateAudioFileMetadata.bind(this))
 | 
					    this.router.get('/items/:id/audio-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.updateAudioFileMetadata.bind(this))
 | 
				
			||||||
    this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this))
 | 
					    this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this))
 | 
				
			||||||
    this.router.post('/items/:id/open-feed', LibraryItemController.middleware.bind(this), LibraryItemController.openRSSFeed.bind(this))
 | 
					    this.router.post('/items/:id/open-feed', LibraryItemController.middleware.bind(this), LibraryItemController.openRSSFeed.bind(this))
 | 
				
			||||||
 | 
				
			|||||||
@ -80,7 +80,7 @@ function elapsedPretty(seconds) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
module.exports.elapsedPretty = elapsedPretty
 | 
					module.exports.elapsedPretty = elapsedPretty
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function secondsToTimestamp(seconds, includeMs = false) {
 | 
					function secondsToTimestamp(seconds, includeMs = false, alwaysIncludeHours = false) {
 | 
				
			||||||
  var _seconds = seconds
 | 
					  var _seconds = seconds
 | 
				
			||||||
  var _minutes = Math.floor(seconds / 60)
 | 
					  var _minutes = Math.floor(seconds / 60)
 | 
				
			||||||
  _seconds -= _minutes * 60
 | 
					  _seconds -= _minutes * 60
 | 
				
			||||||
@ -91,6 +91,9 @@ function secondsToTimestamp(seconds, includeMs = false) {
 | 
				
			|||||||
  _seconds = Math.floor(_seconds)
 | 
					  _seconds = Math.floor(_seconds)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  var msString = '.' + (includeMs ? ms.toFixed(3) : '0.0').split('.')[1]
 | 
					  var msString = '.' + (includeMs ? ms.toFixed(3) : '0.0').split('.')[1]
 | 
				
			||||||
 | 
					  if (alwaysIncludeHours) {
 | 
				
			||||||
 | 
					    return `${_hours.toString().padStart(2, '0')}:${_minutes.toString().padStart(2, '0')}:${_seconds.toString().padStart(2, '0')}${msString}`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  if (!_hours) {
 | 
					  if (!_hours) {
 | 
				
			||||||
    return `${_minutes}:${_seconds.toString().padStart(2, '0')}${msString}`
 | 
					    return `${_minutes}:${_seconds.toString().padStart(2, '0')}${msString}`
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -177,8 +177,8 @@ function parseTags(format, verbose) {
 | 
				
			|||||||
    file_tag_comment: tryGrabTags(format, 'comment', 'comm', 'com'),
 | 
					    file_tag_comment: tryGrabTags(format, 'comment', 'comm', 'com'),
 | 
				
			||||||
    file_tag_description: tryGrabTags(format, 'description', 'desc'),
 | 
					    file_tag_description: tryGrabTags(format, 'description', 'desc'),
 | 
				
			||||||
    file_tag_genre: tryGrabTags(format, 'genre', 'tcon', 'tco'),
 | 
					    file_tag_genre: tryGrabTags(format, 'genre', 'tcon', 'tco'),
 | 
				
			||||||
    file_tag_series: tryGrabTags(format, 'series', 'show'),
 | 
					    file_tag_series: tryGrabTags(format, 'series', 'show', 'mvin'),
 | 
				
			||||||
    file_tag_seriespart: tryGrabTags(format, 'series-part', 'episode_id'),
 | 
					    file_tag_seriespart: tryGrabTags(format, 'series-part', 'episode_id', 'mvnm'),
 | 
				
			||||||
    file_tag_isbn: tryGrabTags(format, 'isbn'),
 | 
					    file_tag_isbn: tryGrabTags(format, 'isbn'),
 | 
				
			||||||
    file_tag_language: tryGrabTags(format, 'language', 'lang'),
 | 
					    file_tag_language: tryGrabTags(format, 'language', 'lang'),
 | 
				
			||||||
    file_tag_asin: tryGrabTags(format, 'asin'),
 | 
					    file_tag_asin: tryGrabTags(format, 'asin'),
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										87
									
								
								server/utils/toneHelpers.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								server/utils/toneHelpers.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,87 @@
 | 
				
			|||||||
 | 
					const tone = require('node-tone')
 | 
				
			||||||
 | 
					const fs = require('../libs/fsExtra')
 | 
				
			||||||
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
 | 
					const { secondsToTimestamp } = require('./index')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.writeToneChaptersFile = (chapters, filePath) => {
 | 
				
			||||||
 | 
					  var chaptersTxt = ''
 | 
				
			||||||
 | 
					  for (const chapter of chapters) {
 | 
				
			||||||
 | 
					    chaptersTxt += `${secondsToTimestamp(chapter.start, true, true)} ${chapter.title}\n`
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return fs.writeFile(filePath, chaptersTxt)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.getToneMetadataObject = (libraryItem, chaptersFile) => {
 | 
				
			||||||
 | 
					  const coverPath = libraryItem.media.coverPath
 | 
				
			||||||
 | 
					  const bookMetadata = libraryItem.media.metadata
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const metadataObject = {
 | 
				
			||||||
 | 
					    'Title': bookMetadata.title || '',
 | 
				
			||||||
 | 
					    'Album': bookMetadata.title || '',
 | 
				
			||||||
 | 
					    'TrackTotal': libraryItem.media.tracks.length
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const additionalFields = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (bookMetadata.subtitle) {
 | 
				
			||||||
 | 
					    metadataObject['Subtitle'] = bookMetadata.subtitle
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (bookMetadata.authorName) {
 | 
				
			||||||
 | 
					    metadataObject['Artist'] = bookMetadata.authorName
 | 
				
			||||||
 | 
					    metadataObject['AlbumArtist'] = bookMetadata.authorName
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (bookMetadata.description) {
 | 
				
			||||||
 | 
					    metadataObject['Comment'] = bookMetadata.description
 | 
				
			||||||
 | 
					    metadataObject['Description'] = bookMetadata.description
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (bookMetadata.narratorName) {
 | 
				
			||||||
 | 
					    metadataObject['Narrator'] = bookMetadata.narratorName
 | 
				
			||||||
 | 
					    metadataObject['Composer'] = bookMetadata.narratorName
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (bookMetadata.firstSeriesName) {
 | 
				
			||||||
 | 
					    metadataObject['MovementName'] = bookMetadata.firstSeriesName
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (bookMetadata.firstSeriesSequence) {
 | 
				
			||||||
 | 
					    metadataObject['Movement'] = bookMetadata.firstSeriesSequence
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (bookMetadata.genres.length) {
 | 
				
			||||||
 | 
					    metadataObject['Genre'] = bookMetadata.genres.join('/')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (bookMetadata.publisher) {
 | 
				
			||||||
 | 
					    metadataObject['Publisher'] = bookMetadata.publisher
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (bookMetadata.asin) {
 | 
				
			||||||
 | 
					    additionalFields.push(`ASIN=${bookMetadata.asin}`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (bookMetadata.isbn) {
 | 
				
			||||||
 | 
					    additionalFields.push(`ISBN=${bookMetadata.isbn}`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (coverPath) {
 | 
				
			||||||
 | 
					    metadataObject['CoverFile'] = coverPath
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (parsePublishedYear(bookMetadata.publishedYear)) {
 | 
				
			||||||
 | 
					    metadataObject['PublishingDate'] = parsePublishedYear(bookMetadata.publishedYear)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (chaptersFile) {
 | 
				
			||||||
 | 
					    metadataObject['ChaptersFile'] = chaptersFile
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (additionalFields.length) {
 | 
				
			||||||
 | 
					    metadataObject['AdditionalFields'] = additionalFields
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return metadataObject
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.tagAudioFile = (filePath, payload) => {
 | 
				
			||||||
 | 
					  return tone.tag(filePath, payload).then((data) => {
 | 
				
			||||||
 | 
					    return true
 | 
				
			||||||
 | 
					  }).catch((error) => {
 | 
				
			||||||
 | 
					    Logger.error(`[toneHelpers] tagAudioFile: Failed for "${filePath}"`, error)
 | 
				
			||||||
 | 
					    return false
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function parsePublishedYear(publishedYear) {
 | 
				
			||||||
 | 
					  if (isNaN(publishedYear) || !publishedYear || Number(publishedYear) <= 0) return null
 | 
				
			||||||
 | 
					  return `01/01/${publishedYear}`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user