mediasnacks 0.30.3 → 0.32.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/avif.js +1 -1
- package/src/cli.js +7 -6
- package/src/detectdups.js +1 -1
- package/src/dropdups.js +8 -7
- package/src/edgespic.js +1 -1
- package/src/frameseq.js +1 -1
- package/src/hev1tohvc1.js +1 -1
- package/src/info.js +65 -0
- package/src/moov2front.js +1 -1
- package/src/prores.js +4 -4
- package/src/resize.js +1 -1
- package/src/sqcrop.js +1 -1
- package/src/ssim.js +1 -1
- package/src/utils/ffmpeg.js +64 -0
- package/src/utils/formatSeconds.js +32 -0
- package/src/utils/parseOptions.js +15 -22
- package/src/utils/parseTimecode.js +4 -4
- package/src/utils/printProgress.js +34 -0
- package/src/utils/subprocess.js +1 -15
- package/src/utils/test-utils.js +4 -13
- package/src/vtrim.js +1 -1
package/README.md
CHANGED
package/index.js
CHANGED
package/package.json
CHANGED
package/src/avif.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join, basename, dirname } from 'node:path'
|
|
2
2
|
import { parseOptions } from './utils/parseOptions.js'
|
|
3
3
|
import { replaceExt, lstat } from './utils/fs-utils.js'
|
|
4
|
-
import { ffmpeg } from './utils/
|
|
4
|
+
import { ffmpeg } from './utils/ffmpeg.js'
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
const HELP = `
|
package/src/cli.js
CHANGED
|
@@ -17,7 +17,8 @@ const COMMANDS = {
|
|
|
17
17
|
frameseq: ['./frameseq.js', 'Converts video to sequence of PNGs'],
|
|
18
18
|
countframes: ['./countframes.js', 'Counts frames in a video'],
|
|
19
19
|
ssim: ['./ssim.js', 'Computes SSIM between two images'],
|
|
20
|
-
gif: ['./gif.js', 'Video to GIF
|
|
20
|
+
gif: ['./gif.js', 'Video to GIF'],
|
|
21
|
+
info: ['./info.js', 'Prints video stream attributes\n'],
|
|
21
22
|
|
|
22
23
|
detectdups: ['./detectdups.js', 'Detects duplicate frames in a video'],
|
|
23
24
|
dropdups: ['./dropdups.js', 'Removes duplicate frames in a video'],
|
|
@@ -54,7 +55,7 @@ SYNOPSIS
|
|
|
54
55
|
|
|
55
56
|
COMMANDS
|
|
56
57
|
${commandsSummary().map(([cmd, desc]) =>
|
|
57
|
-
` ${styleText('bold', cmd.padEnd(12
|
|
58
|
+
` ${styleText('bold', cmd.padEnd(12))}\t${desc}`).join('\n')}
|
|
58
59
|
`.trim()
|
|
59
60
|
|
|
60
61
|
|
|
@@ -76,11 +77,11 @@ async function main() {
|
|
|
76
77
|
if (!opt) throw HELP
|
|
77
78
|
if (!Object.hasOwn(COMMANDS, opt)) throw `'${opt}' is not a command. See mediasnacks --help\n`
|
|
78
79
|
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
-
await (await import(
|
|
80
|
+
const prog = COMMANDS[opt][0]
|
|
81
|
+
if (prog.endsWith('.js'))
|
|
82
|
+
await (await import(prog)).default()
|
|
82
83
|
else
|
|
83
|
-
spawn(join(import.meta.dirname,
|
|
84
|
+
spawn(join(import.meta.dirname, prog), args, { stdio: 'inherit' })
|
|
84
85
|
.on('exit', process.exit)
|
|
85
86
|
}
|
|
86
87
|
|
package/src/detectdups.js
CHANGED
package/src/dropdups.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { resolve, parse, format } from 'node:path'
|
|
2
|
-
import { parseOptions } from './utils/parseOptions.js'
|
|
3
|
-
import { ffmpeg, run } from './utils/subprocess.js'
|
|
4
2
|
import { ProresProfiles } from './prores.js'
|
|
3
|
+
import { parseOptions } from './utils/parseOptions.js'
|
|
4
|
+
import { ffmpegWithProgress } from './utils/ffmpeg.js'
|
|
5
|
+
import { infoSummary } from './info.js'
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
const PROFILE = ProresProfiles.default
|
|
@@ -15,7 +16,7 @@ DESCRIPTION
|
|
|
15
16
|
|
|
16
17
|
OPTIONS
|
|
17
18
|
-n, --dup-frame-num <n> Known frame interval to drop.
|
|
18
|
-
|
|
19
|
+
Default: n=0, which auto-detects repeated frames (slower)
|
|
19
20
|
|
|
20
21
|
EXAMPLES
|
|
21
22
|
Use n=2 when every other frame is repeated:
|
|
@@ -38,14 +39,14 @@ export default async function main() {
|
|
|
38
39
|
if (dupFrameNum && !Number.isInteger(+dupFrameNum))
|
|
39
40
|
throw usage('Invalid -n. It must be a positive integer.')
|
|
40
41
|
|
|
41
|
-
for (const file of files)
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
console.log(await infoSummary(file))
|
|
42
44
|
await dropdups(resolve(file), dupFrameNum)
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export async function dropdups(video, dupFrameNum) {
|
|
46
|
-
await
|
|
47
|
-
'-v', 'error',
|
|
48
|
-
'-stats',
|
|
49
|
+
await ffmpegWithProgress(video, [
|
|
49
50
|
'-an',
|
|
50
51
|
'-i', video,
|
|
51
52
|
'-vf', dupFrameNum
|
package/src/edgespic.js
CHANGED
|
@@ -3,7 +3,7 @@ import { basename, extname, join, parse } from 'node:path'
|
|
|
3
3
|
import { mkDir } from './utils/fs-utils.js'
|
|
4
4
|
import { videoAttrs } from './utils/videoAttrs.js'
|
|
5
5
|
import { parseOptions } from './utils/parseOptions.js'
|
|
6
|
-
import { ffmpeg } from './utils/
|
|
6
|
+
import { ffmpeg } from './utils/ffmpeg.js'
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
const WIDTH = 640
|
package/src/frameseq.js
CHANGED
|
@@ -2,7 +2,7 @@ import { basename, extname, join, parse } from 'node:path'
|
|
|
2
2
|
|
|
3
3
|
import { mkDir } from './utils/fs-utils.js'
|
|
4
4
|
import { parseOptions } from './utils/parseOptions.js'
|
|
5
|
-
import { ffmpeg } from './utils/
|
|
5
|
+
import { ffmpeg } from './utils/ffmpeg.js'
|
|
6
6
|
import { countframes } from './countframes.js'
|
|
7
7
|
|
|
8
8
|
|
package/src/hev1tohvc1.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseOptions } from './utils/parseOptions.js'
|
|
2
2
|
import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
|
|
3
|
-
import { ffmpeg } from './utils/subprocess.js'
|
|
4
3
|
import { videoAttrs } from './utils/videoAttrs.js'
|
|
4
|
+
import { ffmpeg } from './utils/ffmpeg.js'
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
const HELP = `
|
package/src/info.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { parseOptions } from './utils/parseOptions.js'
|
|
2
|
+
import { videoAttrs } from './utils/videoAttrs.js'
|
|
3
|
+
import { formatSeconds, cleanDecimals } from './utils/formatSeconds.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const HELP = `
|
|
7
|
+
SYNOPSIS
|
|
8
|
+
mediasnacks info [-a | --all] <files>
|
|
9
|
+
|
|
10
|
+
DESCRIPTION
|
|
11
|
+
Prints video or image attributes using ffprobe. By default, it’s similar to
|
|
12
|
+
\`ls\` but prints the: width, height, fps, duration, codec, and filename.
|
|
13
|
+
|
|
14
|
+
OPTIONS
|
|
15
|
+
-a, --all Prints everything as JSON
|
|
16
|
+
|
|
17
|
+
EXAMPLES
|
|
18
|
+
Short summary of each match:
|
|
19
|
+
mediasnacks info *.mp4
|
|
20
|
+
|
|
21
|
+
Sort by fps:
|
|
22
|
+
mediasnacks info *.* | sort -k3,3n
|
|
23
|
+
|
|
24
|
+
Move 60fps videos into 60fps/ subdir:
|
|
25
|
+
FPS=60
|
|
26
|
+
DIR=\${FPS}fps
|
|
27
|
+
mkdir -p \$DIR
|
|
28
|
+
mediasnacks info *.* |
|
|
29
|
+
grep \${FPS}fps |
|
|
30
|
+
awk -F\\t '{print $NF}' |
|
|
31
|
+
while read -r f; do
|
|
32
|
+
mv -- "$f" \$DIR/
|
|
33
|
+
done
|
|
34
|
+
`
|
|
35
|
+
|
|
36
|
+
export default async function main() {
|
|
37
|
+
const { values, files, usage } = await parseOptions(HELP, {
|
|
38
|
+
all: { short: 'a', type: 'boolean' }
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (!files[0]) throw usage('No video file specified')
|
|
42
|
+
|
|
43
|
+
for (const video of files)
|
|
44
|
+
if (values.all)
|
|
45
|
+
console.log(JSON.stringify(await videoAttrs(video), '', 2))
|
|
46
|
+
else
|
|
47
|
+
console.log(`${await infoSummary(video)}\t${video}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
export async function infoSummary(video) {
|
|
52
|
+
const v = await videoAttrs(video)
|
|
53
|
+
return [
|
|
54
|
+
String(v.width).padStart(4),
|
|
55
|
+
String(v.height).padStart(4),
|
|
56
|
+
`${fps(v.r_frame_rate)}fps`.padStart(7),
|
|
57
|
+
formatSeconds(v.duration, 0).padStart(8),
|
|
58
|
+
v.codec_name
|
|
59
|
+
].join('\t')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fps(rFrameRate) {
|
|
63
|
+
const [num, den] = rFrameRate.split('/').map(Number)
|
|
64
|
+
return cleanDecimals((num / den).toFixed(2))
|
|
65
|
+
}
|
package/src/moov2front.js
CHANGED
package/src/prores.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve, parse, join } from 'node:path'
|
|
2
2
|
import { parseOptions } from './utils/parseOptions.js'
|
|
3
|
-
import {
|
|
3
|
+
import { ffmpegWithProgress } from './utils/ffmpeg.js'
|
|
4
|
+
import { infoSummary } from './info.js'
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
// https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
|
|
@@ -68,13 +69,12 @@ export default async function main() {
|
|
|
68
69
|
const output = join(dir, `${name}.prores.mov`)
|
|
69
70
|
|
|
70
71
|
const { profile, start, end } = values
|
|
72
|
+
console.log(await infoSummary(video))
|
|
71
73
|
await prores({ video, profile, start, end, output })
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
export async function prores({ video, profile, start, end, output }) {
|
|
75
|
-
await
|
|
76
|
-
'-v', 'error',
|
|
77
|
-
'-stats',
|
|
77
|
+
await ffmpegWithProgress(video, [
|
|
78
78
|
start ? ['-ss', start] : [],
|
|
79
79
|
end ? ['-to', end] : [],
|
|
80
80
|
'-i', video,
|
package/src/resize.js
CHANGED
|
@@ -3,8 +3,8 @@ import { rename } from 'node:fs/promises'
|
|
|
3
3
|
|
|
4
4
|
import { parseOptions } from './utils/parseOptions.js'
|
|
5
5
|
import { isFile, uniqueFilenameFor } from './utils/fs-utils.js'
|
|
6
|
-
import { ffmpeg } from './utils/subprocess.js'
|
|
7
6
|
import { videoAttrs } from './utils/videoAttrs.js'
|
|
7
|
+
import { ffmpeg } from './utils/ffmpeg.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
const HELP = `
|
package/src/sqcrop.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { rename } from 'node:fs/promises'
|
|
3
3
|
|
|
4
|
-
import { ffmpeg } from './utils/subprocess.js'
|
|
5
4
|
import { lstat, uniqueFilenameFor } from './utils/fs-utils.js'
|
|
6
5
|
import { parseOptions } from './utils/parseOptions.js'
|
|
7
6
|
import { videoAttrs } from './utils/videoAttrs.js'
|
|
7
|
+
import { ffmpeg } from './utils/ffmpeg.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
const HELP = `
|
package/src/ssim.js
CHANGED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { printProgress, showCursor } from './printProgress.js'
|
|
4
|
+
import { videoAttrs } from './videoAttrs.js'
|
|
5
|
+
import { runSilently } from './subprocess.js'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async function assertUserHasFFmpeg() {
|
|
9
|
+
try {
|
|
10
|
+
await runSilently('ffmpeg', ['-version'])
|
|
11
|
+
await runSilently('ffprobe', ['-version'])
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new Error('ffmpeg not found. Please install ffmpeg.')
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
export async function ffmpeg(args) {
|
|
20
|
+
await assertUserHasFFmpeg()
|
|
21
|
+
return runSilently('ffmpeg', args)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
export async function ffmpegWithProgress(input, args, onProgress = printProgress) {
|
|
26
|
+
await assertUserHasFFmpeg()
|
|
27
|
+
const µsVideoDuration = 1e6 * (await videoAttrs(input)).duration
|
|
28
|
+
|
|
29
|
+
const p = spawn('ffmpeg', [
|
|
30
|
+
'-v', 'error',
|
|
31
|
+
'-nostats',
|
|
32
|
+
'-progress', 'pipe:1',
|
|
33
|
+
...args
|
|
34
|
+
], { stdio: ['inherit', 'pipe', 'pipe'] })
|
|
35
|
+
|
|
36
|
+
const startTime = performance.now()
|
|
37
|
+
p.stdout.on('data', chunk => {
|
|
38
|
+
const text = chunk.toString()
|
|
39
|
+
const msElapsed = performance.now() - startTime
|
|
40
|
+
if (text.includes('progress=continue')) {
|
|
41
|
+
const m = text.match(/out_time_us=(\d+)/)
|
|
42
|
+
const progress = Number(m[1]) / µsVideoDuration
|
|
43
|
+
const msETA = msElapsed * (1 - progress) / progress
|
|
44
|
+
onProgress(progress, msElapsed, msETA)
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
onProgress(1, msElapsed, 0)
|
|
48
|
+
})
|
|
49
|
+
process.on('SIGINT', () => {
|
|
50
|
+
p?.kill('SIGINT')
|
|
51
|
+
showCursor()
|
|
52
|
+
console.log('\nAborted')
|
|
53
|
+
process.exit(128 + os.constants.signals.SIGINT)
|
|
54
|
+
})
|
|
55
|
+
p.stderr.pipe(process.stderr)
|
|
56
|
+
|
|
57
|
+
await new Promise((resolve, reject) => {
|
|
58
|
+
p.on('error', reject)
|
|
59
|
+
p.on('close', code => {
|
|
60
|
+
if (code === 0) resolve()
|
|
61
|
+
else reject(Error(`ffmpeg failed with code ${code}`))
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes trailing zeros and a trailing decimal point.
|
|
3
|
+
*
|
|
4
|
+
* Examples:
|
|
5
|
+
* cleanDecimals(3.1400) -> "3.14"
|
|
6
|
+
* cleanDecimals(5.0) -> "5"
|
|
7
|
+
*/
|
|
8
|
+
export const cleanDecimals = Number
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Converts seconds to a string like "9h9m9s".
|
|
12
|
+
*
|
|
13
|
+
* Examples:
|
|
14
|
+
* formatSeconds(1.1) -> "1.1s"
|
|
15
|
+
* formatSeconds(3661, 2) -> "1h1m1s"
|
|
16
|
+
* formatSeconds(3661.0, 2) -> "1h1m1s"
|
|
17
|
+
* formatSeconds(3661.25, 2) -> "1h1m1.25s"
|
|
18
|
+
*/
|
|
19
|
+
export function formatSeconds(seconds, maxDecimals = 2) {
|
|
20
|
+
if (!Number.isFinite(+seconds))
|
|
21
|
+
return ''
|
|
22
|
+
const intSeconds = seconds | 0
|
|
23
|
+
const partialSeconds = seconds % 60
|
|
24
|
+
const minutes = (intSeconds % 3600) / 60 | 0
|
|
25
|
+
const hours = intSeconds / 3600 | 0
|
|
26
|
+
|
|
27
|
+
let result = ''
|
|
28
|
+
if (hours) result += hours + 'h'
|
|
29
|
+
if (minutes) result += minutes + 'm'
|
|
30
|
+
if (partialSeconds || !result) result += cleanDecimals(partialSeconds.toFixed(maxDecimals)) + 's'
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
@@ -10,48 +10,41 @@ const glob = promisify(_glob)
|
|
|
10
10
|
* @param {Partial<import('node:util').ParseArgsConfig>} [config]
|
|
11
11
|
*/
|
|
12
12
|
export async function parseOptions(helpText, options = {}, config = {}) {
|
|
13
|
+
helpText = helpText.trim()
|
|
13
14
|
options.help = { short: 'h', type: 'boolean' }
|
|
14
15
|
|
|
15
|
-
const { values, positionals
|
|
16
|
+
const { values, positionals } = parseArgs({
|
|
16
17
|
args: process.argv.slice(3),
|
|
17
18
|
allowPositionals: true,
|
|
18
19
|
options,
|
|
19
|
-
...config
|
|
20
|
-
tokens: true
|
|
20
|
+
...config
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
if (values.help) {
|
|
24
|
-
console.log(helpText
|
|
24
|
+
console.log(helpText)
|
|
25
25
|
process.exit(0)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
return {
|
|
29
29
|
values,
|
|
30
30
|
positionals,
|
|
31
|
-
files: await resolveGlobs(positionals
|
|
31
|
+
files: await resolveGlobs(positionals),
|
|
32
32
|
usage: err => err
|
|
33
|
-
? styleText('redBright',
|
|
33
|
+
? styleText('redBright', err + '\n') + helpText
|
|
34
34
|
: helpText
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
async function resolveGlobs(arr
|
|
39
|
-
const terminatorIndex = tokens.find(t => t.kind === 'option-terminator')?.index ?? -1
|
|
38
|
+
async function resolveGlobs(arr) {
|
|
40
39
|
const set = new Set()
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (terminatorIndex !== -1)
|
|
52
|
-
for (const literal of arr.slice(terminatorIndex))
|
|
53
|
-
set.add(literal)
|
|
54
|
-
|
|
40
|
+
for (const arg of arr) {
|
|
41
|
+
const matches = await glob(arg)
|
|
42
|
+
if (matches.length)
|
|
43
|
+
for (const file of matches)
|
|
44
|
+
set.add(file)
|
|
45
|
+
else
|
|
46
|
+
set.add(arg)
|
|
47
|
+
}
|
|
55
48
|
return Array.from(set)
|
|
56
49
|
}
|
|
57
50
|
|
|
@@ -6,11 +6,11 @@ export function parseTimecode(time) {
|
|
|
6
6
|
if (parts.some(isNaN) || parts.length > 3)
|
|
7
7
|
throw new Error(`Invalid time: ${time}`)
|
|
8
8
|
|
|
9
|
-
// HH:MM:SS or HH:MM:SS.mmm
|
|
10
|
-
|
|
9
|
+
if (parts.length === 3) // HH:MM:SS or HH:MM:SS.mmm
|
|
10
|
+
return (3600 * parts[0]) + (60 * parts[1]) + parts[2]
|
|
11
11
|
|
|
12
|
-
// MM:SS or MM:SS.mmm
|
|
13
|
-
|
|
12
|
+
if (parts.length === 2) // MM:SS or MM:SS.mmm
|
|
13
|
+
return (60 * parts[0]) + parts[1]
|
|
14
14
|
|
|
15
15
|
return parts[0]
|
|
16
16
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { formatSeconds } from './formatSeconds.js'
|
|
2
|
+
|
|
3
|
+
const HIDE_CURSOR = '\x1b[?25l'
|
|
4
|
+
const SHOW_CURSOR = '\x1b[?25h'
|
|
5
|
+
const ERASE_TO_END = '\x1b[K'
|
|
6
|
+
|
|
7
|
+
export const showCursor = () => process.stdout.write(SHOW_CURSOR)
|
|
8
|
+
|
|
9
|
+
export function printProgress(progress, msElapsed, msETA) {
|
|
10
|
+
const elapsed = msElapsed
|
|
11
|
+
? ` • ${formatSeconds(msElapsed / 1000, 0)}`
|
|
12
|
+
: ''
|
|
13
|
+
const eta = msETA
|
|
14
|
+
? ` • ETA ${formatSeconds(msETA / 1000, 0)}`
|
|
15
|
+
: ''
|
|
16
|
+
const percent = progress === 1
|
|
17
|
+
? '100%'
|
|
18
|
+
: `${(progress * 100).toFixed(1)}%`
|
|
19
|
+
process.stdout.write(HIDE_CURSOR)
|
|
20
|
+
process.stdout.write(`\r${progressBar(progress)} ${percent}${eta}${elapsed}${ERASE_TO_END}`)
|
|
21
|
+
if (progress === 1)
|
|
22
|
+
process.stdout.write('\n' + SHOW_CURSOR)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function progressBar(progress, width = 44) {
|
|
26
|
+
width-- // for partial char
|
|
27
|
+
const nFull = (width * progress) | 0
|
|
28
|
+
const fPartial = (width * progress) - nFull
|
|
29
|
+
const nRemaining = width - nFull
|
|
30
|
+
|
|
31
|
+
const partials = ' ▏▎▍▌▋▊▉'
|
|
32
|
+
const partial = partials.at(partials.length * fPartial)
|
|
33
|
+
return '█'.repeat(nFull) + partial + '•'.repeat(nRemaining)
|
|
34
|
+
}
|
package/src/utils/subprocess.js
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
async function assertUserHasFFmpeg() {
|
|
5
|
-
try {
|
|
6
|
-
await runSilently('ffmpeg', ['-version'])
|
|
7
|
-
await runSilently('ffprobe', ['-version'])
|
|
8
|
-
}
|
|
9
|
-
catch {
|
|
10
|
-
throw new Error('ffmpeg not found. Please install ffmpeg.')
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function ffmpeg(args) {
|
|
15
|
-
await assertUserHasFFmpeg()
|
|
16
|
-
return runSilently('ffmpeg', args)
|
|
17
|
-
}
|
|
18
|
-
|
|
19
4
|
export async function runSilently(program, args) {
|
|
20
5
|
return new Promise((resolve, reject) => {
|
|
21
6
|
const stdout = []
|
|
@@ -38,6 +23,7 @@ export async function runSilently(program, args) {
|
|
|
38
23
|
})
|
|
39
24
|
}
|
|
40
25
|
|
|
26
|
+
|
|
41
27
|
export async function run(program, args) {
|
|
42
28
|
return new Promise((resolve, reject) => {
|
|
43
29
|
const p = spawn(program, args, { stdio: ['inherit', 'pipe', 'pipe'] })
|
package/src/utils/test-utils.js
CHANGED
|
@@ -5,19 +5,10 @@ import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
|
5
5
|
|
|
6
6
|
const rel = f => join(import.meta.dirname, f)
|
|
7
7
|
|
|
8
|
-
export
|
|
9
|
-
return mkdtempSync(join(tmpdir(), prefix))
|
|
10
|
-
}
|
|
8
|
+
export const cli = (...args) => spawnSync(rel('../cli.js'), args)
|
|
11
9
|
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
}
|
|
10
|
+
export const dir = (...args) => mkdirSync(join(...args), { recursive: true })
|
|
11
|
+
export const touch = (...args) => writeFileSync(join(...args), '')
|
|
15
12
|
|
|
16
|
-
export
|
|
17
|
-
return mkdirSync(join(...args), { recursive: true })
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function touch(...args) {
|
|
21
|
-
return writeFileSync(join(...args), '')
|
|
22
|
-
}
|
|
13
|
+
export const mkTempDir = (prefix = 'test-') => mkdtempSync(join(tmpdir(), prefix))
|
|
23
14
|
|