Skip to content

Instantly share code, notes, and snippets.

@gcoda
Created July 8, 2019 21:25
Show Gist options
  • Save gcoda/d0194f1294e4c03eb12d8b0eff9076f4 to your computer and use it in GitHub Desktop.
Save gcoda/d0194f1294e4c03eb12d8b0eff9076f4 to your computer and use it in GitHub Desktop.
Split M4B books chapters
/**
* Needs MP4Box binary to work
* $> node split.js "Atomic Habits.m4b"
*/
const { execSync, exec } = require('child_process')
const cheerio = require('cheerio')
const escape = string =>
string
.split('')
.map(char => (char.match(/[a-z0-9]/i) ? char : '\\' + char))
.join('')
const exeCat = command =>
new Promise((resolve, reject) => {
exec(
command,
{ maxBuffer: 1024 * 1024 * 100 },
(error, stdout, stderr) => {
resolve(stdout.toString() || stderr.toString())
if (error !== null) {
reject(error)
}
}
)
})
const timeToSeconds = time => {
const [h = 0, m = 0, s = 0] = String(time)
.split(':')
.map(parseFloat)
const seconds = h * 60 * 60 + m * 60 + Math.floor(s)
return seconds
}
const pad = (n = 0, l = 2) => `${1e12}${n}`.slice(l * -1)
const splitByChapter = inputFileName => {
const inputFile = escape(inputFileName)
const inputName = inputFileName
.split('.')
.slice(0, -1)
.join('.')
return exeCat(`MP4Box -diso -std ${inputFile} | cat`)
.then(xml =>
cheerio.load(xml, { xml: { normalizeWhitespace: true } })
)
.then($ =>
$('ChapterListBox')
.children()
.map((i, el) => $(el).attr())
.toArray()
)
.then(list =>
Promise.all(
list.map(async ({ name, startTime }, i) => {
const start = timeToSeconds(startTime)
const fileName = escape(
`${inputName} - ${pad(i, 3)} - ${name}`
)
let end = ''
if (list[i + 1]) {
end = timeToSeconds(list[i + 1].startTime)
} else {
const info = await exeCat(
`MP4Box -info ${inputFile} | cat`
)
const durationMatch = info.match(
/(?:Computed Duration) (\d{2}:\d{2}:\d{2}.\d+)/m
)
end = durationMatch
? timeToSeconds(durationMatch[1])
: ''
}
const chap = await exeCat(
`MP4Box -splitx ${start}:${end} ${inputFile} -out ${fileName}.mp3`
)
return chap
})
)
)
}
if (process.argv[2])
splitByChapter(process.argv[2])
.then(chapters => (Array.isArray(chapters) ? chapters : []))
.then(results =>
results
.map(out =>
out
.split(/\r|\n/)
.filter(line => !line.match('ISO File Writing'))
.filter(line => !line.match('Splitting'))
.map(line => line.trim())
.filter(String)
)
.map(console.log)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment