diff --git a/README.md b/README.md index 387ab9f..dc99c91 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,11 @@ Note: mpv-cut should have its own folder inside your scripts folder. (`scripts/m `script-opts/mpv-cut.conf`: -- `output_dir` - The output directory for clips, can be relative or absolute. Defaults to `.`, which will place clips in the same directory as the original video. +- `output_dir` - The output directory for cuts, can be relative or absolute. + - Default value: `.` (will place cuts in the same directory as the original video) +- `multi_cut_mode` - The mode for handling multiple cuts for a single video. Options: + - `separate`: create separate cut files (default) + - `join`: merge cut files into a single cut. ## usage diff --git a/script-opts/mpv-cut.conf b/script-opts/mpv-cut.conf index fc462e0..7f93fdd 100644 --- a/script-opts/mpv-cut.conf +++ b/script-opts/mpv-cut.conf @@ -1 +1,2 @@ -output_dir=. \ No newline at end of file +output_dir=. +multi_cut_mode=separate \ No newline at end of file diff --git a/scripts/mpv-cut/main.lua b/scripts/mpv-cut/main.lua index 72780dc..2796e70 100644 --- a/scripts/mpv-cut/main.lua +++ b/scripts/mpv-cut/main.lua @@ -5,7 +5,8 @@ mp.options = require 'mp.options' MAKE_CUTS_SCRIPT_PATH = mp.utils.join_path(mp.get_script_directory(), "make_cuts") options = { - output_dir = "." + output_dir = ".", + multi_cut_mode = "separate" } mp.options.read_options(options, "mpv-cut") @@ -25,6 +26,7 @@ function cut_render() end local cuts_json = mp.utils.format_json(cuts) + local options_json = mp.utils.format_json(options) local inpath = mp.get_property("path") local filename = mp.get_property("filename") @@ -34,7 +36,8 @@ function cut_render() log("Rendering...") print("making cut") - local args = { "node", MAKE_CUTS_SCRIPT_PATH, indir, options.output_dir, filename, cuts_json } + local args = { "node", MAKE_CUTS_SCRIPT_PATH, + indir, options_json, filename, cuts_json } res, err = mp.command_native({ name = "subprocess", @@ -42,7 +45,7 @@ function cut_render() args = args, }) - if res.status == 0 then + if res and res.status == 0 then log("Rendered cuts") else log("Failed to render cuts") diff --git a/scripts/mpv-cut/make_cuts.js b/scripts/mpv-cut/make_cuts.js index 7a36e48..431164b 100644 --- a/scripts/mpv-cut/make_cuts.js +++ b/scripts/mpv-cut/make_cuts.js @@ -13,6 +13,9 @@ const isSubdirectory = (parent, child) => { return relative && !relative.startsWith('..') && !path.isAbsolute(relative); }; +const ffmpegEscapeFilepath = (path) => + path.replaceAll('\\', '\\\\').replaceAll("'", "'\\''"); + function quit(s) { console.log('' + red + s + ', quitting.' + plain + '\n'); return process.exit(1); @@ -53,14 +56,86 @@ async function transferTimestamps(inPath, outPath, offset = 0) { } } +async function ffmpeg(args) { + const cmd = 'ffmpeg'; + const baseArgs = [ + // hide output + '-nostdin', + '-loglevel', + 'error', + // overwrite existing files + '-y', + ]; + + const fullArgs = baseArgs.concat(args); + + const cmdStr = '' + cmd + ' ' + fullArgs.join(' '); + console.log('' + purple + cmdStr + plain + '\n'); + + child_process.spawnSync(cmd, fullArgs, { stdio: 'inherit' }); +} + +async function renderCut(inpath, outpath, start, duration) { + const args = [ + // seek to start before loading file (faster) https://trac.ffmpeg.org/wiki/Seeking#Inputseeking + '-ss', + start, + '-t', + duration, + '-i', + inpath, + // don't re-encode + '-c', + 'copy', + // shift timestamps so they start at 0 + '-avoid_negative_ts', + 'make_zero', + outpath, + ]; + + await ffmpeg(args); + + await transferTimestamps(inpath, outpath); +} + +async function mergeCuts(tempPath, filepaths, outpath) { + // i hate that you have to do a separate command and render each cut separately first, i tried using + // filter_complex for merging with multiple inputs but it wouldn't let me. todo: look into this further + + const mergeFile = path.join(tempPath, 'merging.txt'); + await fs.promises.writeFile( + mergeFile, + filepaths.map((path) => `file '${ffmpegEscapeFilepath(path)}`).join('\n') + ); + + await ffmpeg([ + '-f', + 'concat', + '-safe', + 0, + '-i', + mergeFile, + '-c', + 'copy', + outpath, + ]); + + await fs.promises.unlink(mergeFile); + + for (const path of filepaths) { + await fs.promises.unlink(path); + } +} + async function main() { const argv = process.argv.slice(2); - const [indir, outdir_raw, filename, cutsStr] = argv; + const [indir, optionsStr, filename, cutsStr] = argv; if (!isDir(indir)) quit('Input directory is invalid'); - const outdir = path.resolve(indir, outdir_raw); + const options = JSON.parse(optionsStr); + const outdir = path.resolve(indir, options.output_dir); if (!isDir(outdir)) { if (!isSubdirectory(indir, outdir)) quit('Output directory is invalid'); @@ -72,10 +147,13 @@ async function main() { const cutsMap = JSON.parse(cutsStr); const cuts = Object.values(cutsMap).sort((a, b) => a.start - b.start); + const { name: filename_noext, ext: ext } = path.parse(filename); + + const outpaths = []; + for (const [i, cut] of cuts.entries()) { if (!('end' in cut)) continue; - const { name: filename_noext, ext: ext } = path.parse(filename); const duration = parseFloat(cut.end) - parseFloat(cut.start); const cutName = @@ -91,39 +169,22 @@ async function main() { const inpath = path.join(indir, filename); const outpath = path.join(outdir, cutName); - const cmd = 'ffmpeg'; - const args = [ - '-nostdin', - '-y', - '-loglevel', - 'error', - '-ss', - cut.start, - '-t', - duration, - '-i', - inpath, - '-c', - 'copy', - '-map', - '0', - '-avoid_negative_ts', - 'make_zero', - outpath, - ]; - const progress = '(' + (i + 1) + '/' + cuts.length + ')'; - const cmdStr = '' + cmd + ' ' + args.join(' '); console.log( '' + green + progress + plain + ' ' + inpath + ' ' + green + '->' + plain ); console.log('' + outpath + '\n'); - console.log('' + purple + cmdStr + plain + '\n'); - child_process.spawnSync(cmd, args, { stdio: 'inherit' }); + await renderCut(inpath, outpath, cut.start, duration); + outpaths.push(outpath); + } + + if (outpaths.length > 1 && options.multi_cut_mode == 'merge') { + const cutName = `(${outpaths.length} merged cuts) ` + filename; + const outpath = path.join(outdir, cutName); - await transferTimestamps(inpath, outpath); + await mergeCuts(indir, outpaths, outpath); } return console.log('Done.\n');