diff --git a/client/pages/audiobook/_id/chapters.vue b/client/pages/audiobook/_id/chapters.vue
index 52cb1bbc..f0d83c0b 100644
--- a/client/pages/audiobook/_id/chapters.vue
+++ b/client/pages/audiobook/_id/chapters.vue
@@ -94,9 +94,16 @@
error_outline
+
+
+
+
![]()
+
@@ -246,7 +253,8 @@ export default {
chapterData: null,
showSecondInputs: false,
audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES'],
- hasChanges: false
+ hasChanges: false,
+ showWaveform: {}
}
},
computed: {
@@ -256,6 +264,9 @@ export default {
userToken() {
return this.$store.getters['user/getToken']
},
+ baseUrl() {
+ return process.env.serverUrl
+ },
media() {
return this.libraryItem.media || {}
},
@@ -288,6 +299,9 @@ export default {
}
},
methods: {
+ setShowWaveform(chapterId) {
+ this.$set(this.showWaveform, chapterId, true)
+ },
setChaptersFromTracks() {
let currentStartTime = 0
let index = 0
diff --git a/server/controllers/ToolsController.js b/server/controllers/ToolsController.js
index 3f21c5dd..7302f01a 100644
--- a/server/controllers/ToolsController.js
+++ b/server/controllers/ToolsController.js
@@ -1,4 +1,5 @@
const Logger = require('../Logger')
+const ffmpegHelpers = require('../utils/ffmpegHelpers')
class ToolsController {
constructor() { }
@@ -98,6 +99,32 @@ class ToolsController {
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) {
if (!req.user.isAdminOrUp) {
Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user)
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index 1b2df155..6e34c2bd 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -275,6 +275,7 @@ class ApiRouter {
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/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)
diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js
index b82f9784..4190ffd9 100644
--- a/server/utils/ffmpegHelpers.js
+++ b/server/utils/ffmpegHelpers.js
@@ -152,3 +152,26 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
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
+ })
+}
\ No newline at end of file