Created
July 8, 2019 21:25
-
-
Save gcoda/d0194f1294e4c03eb12d8b0eff9076f4 to your computer and use it in GitHub Desktop.
Split M4B books chapters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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