From c047b8c57d4e5012578c072420d2b73dd5b59c4c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 14 Aug 2021 00:11:39 -0300 Subject: handy combine-album.js utility this isn't exposed via the mtui command so like, just run it directly with node right now lol (this commit also makes "." parse in timestamp positions) --- combine-album.js | 220 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 combine-album.js (limited to 'combine-album.js') diff --git a/combine-album.js b/combine-album.js new file mode 100644 index 0000000..9fd9cf0 --- /dev/null +++ b/combine-album.js @@ -0,0 +1,220 @@ +'use strict' + +// too lazy to use import syntax :) +const { readdir, readFile, stat, writeFile } = require('fs/promises') +const { spawn } = require('child_process') +const { promisifyProcess, parseOptions } = require('./general-util') +const { musicExtensions } = require('./crawlers') +const path = require('path') +const shellescape = require('shell-escape') + +async function timestamps(files) { + const tsData = [] + + let timestamp = 0 + for (const file of files) { + const args = [ + '-print_format', 'json', + '-show_entries', 'stream=codec_name:format', + '-select_streams', 'a:0', + '-v', 'quiet', + file + ] + + const ffprobe = spawn('ffprobe', args) + + let data = '' + ffprobe.stdout.on('data', chunk => { + data += chunk + }) + + await promisifyProcess(ffprobe, false) + + let result + try { + result = JSON.parse(data) + } catch (error) { + throw new Error(`Failed to parse ffprobe output - cmd: ffprobe ${args.join(' ')}`) + } + + const duration = parseFloat(result.format.duration) + + tsData.push({ + comment: path.basename(file, path.extname(file)), + timestamp, + timestampEnd: (timestamp += duration) + }) + } + + // Serialize to a nicer format. + for (const ts of tsData) { + ts.timestamp = Math.trunc(ts.timestamp * 100) / 100 + ts.timestampEnd = Math.trunc(ts.timestampEnd * 100) / 100 + } + + return tsData +} + +async function main() { + const validFormats = ['txt', 'json'] + + let files = [] + + const opts = await parseOptions(process.argv.slice(2), { + 'format': { + type: 'value', + validate(value) { + if (validFormats.includes(value)) { + return true + } else { + return `a valid output format (${validFormats.join(', ')})` + } + } + }, + + 'no-concat-list': {type: 'flag'}, + 'concat-list': {type: 'value'}, + + 'out': {type: 'value'}, + 'o': {alias: 'out'}, + + [parseOptions.handleDashless]: opt => files.push(opt) + }) + + if (files.length === 0) { + console.error(`Please provide either a directory (album) or a list of tracks to generate timestamps from.`) + return 1 + } + + if (!opts.format) { + opts.format = 'txt' + } + + let defaultOut = false + let outFromDirectory + if (!opts.out) { + opts.out = `timestamps.${opts.format}` + defaultOut = true + } + + const stats = [] + + { + let errored = false + for (const file of files) { + try { + stats.push(await stat(file)) + } catch (error) { + console.error(`Failed to stat ${file}`) + errored = true + } + } + if (errored) { + console.error(`One or more paths provided failed to stat.`) + console.error(`There are probably permission issues preventing access!`) + return 1 + } + } + + if (stats.some(s => !s.isFile() && !s.isDirectory())) { + console.error(`A path was provided which isn't a file or a directory.`); + console.error(`This utility doesn't know what to do with that!`); + return 1 + } + + if (stats.length > 1 && !stats.every(s => s.isFile())) { + if (stats.some(s => s.isFile())) { + console.error(`Please don't provide a mix of files and directories.`) + } else { + console.error(`Please don't provide more than one directory.`) + } + console.error(`This utility is only capable of generating a timestamps file from either one directory (an album) or a list of (audio) files.`) + return 1 + } + + if (files.length === 1 && stats[0].isDirectory()) { + const dir = files[0] + try { + files = await readdir(dir) + files = files.filter(f => musicExtensions.includes(path.extname(f).slice(1))) + } catch (error) { + console.error(`Failed to read ${dir} as directory.`) + console.error(error) + console.error(`Please provide a readable directory or multiple audio files.`) + return 1 + } + files = files.map(file => path.join(dir, file)) + if (defaultOut) { + opts.out = path.join(path.dirname(dir), path.basename(dir) + '.timestamps.' + opts.format) + outFromDirectory = dir.replace(new RegExp(path.sep + '$'), '') + } + } else if (process.argv.length > 3) { + files = process.argv.slice(2) + } else { + console.error(`Please provide an album directory or multiple audio files.`) + return 1 + } + + let tsData + try { + tsData = await timestamps(files) + } catch (error) { + console.error(`Ran into a code error while processing timestamps:`) + console.error(error) + return 1 + } + + let tsText + switch (opts.format) { + case 'json': + tsText = JSON.stringify(tsData) + '\n' + break + case 'txt': + tsText = tsData.map(t => `${t.timestamp} ${t.comment}`).join('\n') + '\n' + break + } + + if (opts.out === '-') { + process.stdout.write(tsText) + } else { + try { + writeFile(opts.out, tsText) + } catch (error) { + console.error(`Failed to write to output file ${opts.out}`) + console.error(`Confirm path is writeable or pass "--out -" to print to stdout`) + return 1 + } + } + + console.log(`Wrote timestamps to ${opts.out}`) + + if (!opts['no-concat-list']) { + const concatOutput = ( + (defaultOut + ? (outFromDirectory || 'album') + : `/path/to/album`) + + path.extname(files[0])) + + const concatListPath = opts['concat-list'] || `/tmp/combine-album-concat.txt` + try { + await writeFile(concatListPath, files.map(file => `file ${path.resolve(shellescape([file]))}`).join('\n') + '\n') + console.log(`Generated ffmpeg concat list at ${concatListPath}`) + console.log(`# To concat:`) + console.log(`ffmpeg -f concat -safe 0 -i ${shellescape([concatListPath])} -c copy ${shellescape([concatOutput])}`) + } catch (error) { + console.warn(`Failed to generate ffmpeg concat list`) + console.warn(error) + } finally { + console.log(`(Pass --no-concat-list to skip this step)`) + } + } + + return 0 +} + +main().then( + code => process.exit(code), + err => { + console.error(err) + process.exit(1) + }) -- cgit 1.3.0-6-gf8a5 From 39732bd31e13c9a785eac1fc724bc9fc768be2ab Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 14 Aug 2021 00:44:47 -0300 Subject: show timestamp hours column whenever appropriate --- combine-album.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'combine-album.js') diff --git a/combine-album.js b/combine-album.js index 9fd9cf0..946c4c1 100644 --- a/combine-album.js +++ b/combine-album.js @@ -3,7 +3,7 @@ // too lazy to use import syntax :) const { readdir, readFile, stat, writeFile } = require('fs/promises') const { spawn } = require('child_process') -const { promisifyProcess, parseOptions } = require('./general-util') +const { getTimeStringsFromSec, parseOptions, promisifyProcess } = require('./general-util') const { musicExtensions } = require('./crawlers') const path = require('path') const shellescape = require('shell-escape') @@ -164,13 +164,15 @@ async function main() { return 1 } + const duration = tsData[tsData.length - 1].timestampEnd + let tsText switch (opts.format) { case 'json': tsText = JSON.stringify(tsData) + '\n' break case 'txt': - tsText = tsData.map(t => `${t.timestamp} ${t.comment}`).join('\n') + '\n' + tsText = tsData.map(t => `${getTimeStringsFromSec(t.timestamp, duration, true).timeDone} ${t.comment}`).join('\n') + '\n' break } @@ -197,7 +199,7 @@ async function main() { const concatListPath = opts['concat-list'] || `/tmp/combine-album-concat.txt` try { - await writeFile(concatListPath, files.map(file => `file ${path.resolve(shellescape([file]))}`).join('\n') + '\n') + await writeFile(concatListPath, files.map(file => `file ${shellescape([path.resolve(file)])}`).join('\n') + '\n') console.log(`Generated ffmpeg concat list at ${concatListPath}`) console.log(`# To concat:`) console.log(`ffmpeg -f concat -safe 0 -i ${shellescape([concatListPath])} -c copy ${shellescape([concatOutput])}`) -- cgit 1.3.0-6-gf8a5 From 43f1a1dd1b44065663a797603012394c52a9baea Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 13 May 2023 13:31:58 -0300 Subject: use ESM module syntax & update tui-lib Exciting update! This doesn't make any substantial changes exactly but does update the most quickly-archaic parts of older Node code. --- combine-album.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'combine-album.js') diff --git a/combine-album.js b/combine-album.js index 946c4c1..3b57b6c 100644 --- a/combine-album.js +++ b/combine-album.js @@ -1,12 +1,13 @@ 'use strict' -// too lazy to use import syntax :) -const { readdir, readFile, stat, writeFile } = require('fs/promises') -const { spawn } = require('child_process') -const { getTimeStringsFromSec, parseOptions, promisifyProcess } = require('./general-util') -const { musicExtensions } = require('./crawlers') -const path = require('path') -const shellescape = require('shell-escape') +import {readdir, readFile, stat, writeFile} from 'node:fs/promises' +import {spawn} from 'node:child_process' +import path from 'node:path' + +import shellescape from 'shell-escape' + +import {musicExtensions} from './crawlers.js' +import {getTimeStringsFromSec, parseOptions, promisifyProcess} from './general-util.js' async function timestamps(files) { const tsData = [] -- cgit 1.3.0-6-gf8a5