mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-03 19:07:00 -05:00 
			
		
		
		
	API route to generate waveform images
This commit is contained in:
		
							parent
							
								
									4db26f9f79
								
							
						
					
					
						commit
						74652e2e54
					
				@ -94,9 +94,16 @@
 | 
				
			|||||||
                    <span class="material-icons-outlined text-lg">error_outline</span>
 | 
					                    <span class="material-icons-outlined text-lg">error_outline</span>
 | 
				
			||||||
                  </button>
 | 
					                  </button>
 | 
				
			||||||
                </ui-tooltip>
 | 
					                </ui-tooltip>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <button class="w-7 h-7 rounded-full flex items-center justify-center text-white" @click="setShowWaveform(chapter.id)">
 | 
				
			||||||
 | 
					                  <span class="material-icons-outlined text-lg">graphic_eq</span>
 | 
				
			||||||
 | 
					                </button>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div v-if="showWaveform[chapter.id]" :key="`${chapter.id}-waveform`">
 | 
				
			||||||
 | 
					            <img :src="`${baseUrl}/api/tools/item/${libraryItem.id}/waveform?start=${Math.max(0, chapter.start - 10)}&end=${Math.min(mediaDuration, chapter.start + 10)}&token=${userToken}`" />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        </template>
 | 
					        </template>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -246,7 +253,8 @@ export default {
 | 
				
			|||||||
      chapterData: null,
 | 
					      chapterData: null,
 | 
				
			||||||
      showSecondInputs: false,
 | 
					      showSecondInputs: false,
 | 
				
			||||||
      audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES'],
 | 
					      audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES'],
 | 
				
			||||||
      hasChanges: false
 | 
					      hasChanges: false,
 | 
				
			||||||
 | 
					      showWaveform: {}
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
@ -256,6 +264,9 @@ export default {
 | 
				
			|||||||
    userToken() {
 | 
					    userToken() {
 | 
				
			||||||
      return this.$store.getters['user/getToken']
 | 
					      return this.$store.getters['user/getToken']
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    baseUrl() {
 | 
				
			||||||
 | 
					      return process.env.serverUrl
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    media() {
 | 
					    media() {
 | 
				
			||||||
      return this.libraryItem.media || {}
 | 
					      return this.libraryItem.media || {}
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -288,6 +299,9 @@ export default {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
 | 
					    setShowWaveform(chapterId) {
 | 
				
			||||||
 | 
					      this.$set(this.showWaveform, chapterId, true)
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    setChaptersFromTracks() {
 | 
					    setChaptersFromTracks() {
 | 
				
			||||||
      let currentStartTime = 0
 | 
					      let currentStartTime = 0
 | 
				
			||||||
      let index = 0
 | 
					      let index = 0
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
const Logger = require('../Logger')
 | 
					const Logger = require('../Logger')
 | 
				
			||||||
 | 
					const ffmpegHelpers = require('../utils/ffmpegHelpers')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ToolsController {
 | 
					class ToolsController {
 | 
				
			||||||
  constructor() { }
 | 
					  constructor() { }
 | 
				
			||||||
@ -98,6 +99,32 @@ class ToolsController {
 | 
				
			|||||||
    res.sendStatus(200)
 | 
					    res.sendStatus(200)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getAudioFileWaveform(req, res) {
 | 
				
			||||||
 | 
					    let start = Number(req.query.start || 0)
 | 
				
			||||||
 | 
					    let end = Number(req.query.end || 0)
 | 
				
			||||||
 | 
					    if (isNaN(start) || isNaN(end) || start < 0 || end > req.libraryItem.media.duration || end <= start || end - start < 5) {
 | 
				
			||||||
 | 
					      return res.status(400).send('Invalid start/end query params')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const paths = []
 | 
				
			||||||
 | 
					    let currentTime = 0
 | 
				
			||||||
 | 
					    let startOffset = 0
 | 
				
			||||||
 | 
					    for (const track of req.libraryItem.media.tracks) {
 | 
				
			||||||
 | 
					      currentTime += track.duration
 | 
				
			||||||
 | 
					      if (currentTime > start) {
 | 
				
			||||||
 | 
					        if (!paths.length) startOffset = track.startOffset
 | 
				
			||||||
 | 
					        paths.push(track.metadata.path)
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (currentTime > end) {
 | 
				
			||||||
 | 
					        break
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    start -= startOffset
 | 
				
			||||||
 | 
					    end -= startOffset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ffmpegHelpers.generateWaveform(paths, start, end, res)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  middleware(req, res, next) {
 | 
					  middleware(req, res, next) {
 | 
				
			||||||
    if (!req.user.isAdminOrUp) {
 | 
					    if (!req.user.isAdminOrUp) {
 | 
				
			||||||
      Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user)
 | 
					      Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user)
 | 
				
			||||||
 | 
				
			|||||||
@ -275,6 +275,7 @@ class ApiRouter {
 | 
				
			|||||||
    this.router.delete('/tools/item/:id/encode-m4b', ToolsController.middleware.bind(this), ToolsController.cancelM4bEncode.bind(this))
 | 
					    this.router.delete('/tools/item/:id/encode-m4b', ToolsController.middleware.bind(this), ToolsController.cancelM4bEncode.bind(this))
 | 
				
			||||||
    this.router.post('/tools/item/:id/embed-metadata', ToolsController.middleware.bind(this), ToolsController.embedAudioFileMetadata.bind(this))
 | 
					    this.router.post('/tools/item/:id/embed-metadata', ToolsController.middleware.bind(this), ToolsController.embedAudioFileMetadata.bind(this))
 | 
				
			||||||
    this.router.post('/tools/batch/embed-metadata', ToolsController.middleware.bind(this), ToolsController.batchEmbedMetadata.bind(this))
 | 
					    this.router.post('/tools/batch/embed-metadata', ToolsController.middleware.bind(this), ToolsController.batchEmbedMetadata.bind(this))
 | 
				
			||||||
 | 
					    this.router.get('/tools/item/:id/waveform', ToolsController.middleware.bind(this), ToolsController.getAudioFileWaveform.bind(this))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 
 | 
					    // 
 | 
				
			||||||
    // RSS Feed Routes (Admin and up)
 | 
					    // RSS Feed Routes (Admin and up)
 | 
				
			||||||
 | 
				
			|||||||
@ -152,3 +152,26 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
 | 
				
			|||||||
    ffmpeg.run()
 | 
					    ffmpeg.run()
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					module.exports.generateWaveform = (filepaths, start, end, res) => {
 | 
				
			||||||
 | 
					  let ffmpeg = null
 | 
				
			||||||
 | 
					  if (filepaths.length === 1) ffmpeg = Ffmpeg(filepaths[0])
 | 
				
			||||||
 | 
					  else {
 | 
				
			||||||
 | 
					    ffmpeg = Ffmpeg(`concat:${filepaths.join('|')}`)
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  ffmpeg.inputOptions('-ss', start)
 | 
				
			||||||
 | 
					  ffmpeg.inputOptions('-to', end)
 | 
				
			||||||
 | 
					  ffmpeg.complexFilter('aformat=channel_layouts=mono,showwavespic=s=1280x240')
 | 
				
			||||||
 | 
					  ffmpeg.frames(1)
 | 
				
			||||||
 | 
					  ffmpeg.format('image2pipe')
 | 
				
			||||||
 | 
					  ffmpeg.on('start', (cmd) => {
 | 
				
			||||||
 | 
					    Logger.debug(`[FfmpegHelpers] generateWaveform: Cmd: ${cmd}`)
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					  ffmpeg.on('error', (error) => {
 | 
				
			||||||
 | 
					    Logger.error(`[FfmpegHelpers] generateWaveform: Error`, error)
 | 
				
			||||||
 | 
					  }).on('end', () => {
 | 
				
			||||||
 | 
					    Logger.debug(`[FfmpegHelpers] generateWaveform finished`)
 | 
				
			||||||
 | 
					  }).pipe(res, {
 | 
				
			||||||
 | 
					    end: true
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user