mediasnacks 0.27.0 → 0.29.0

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/src/openrand.js CHANGED
@@ -6,27 +6,23 @@ import { parseOptions } from './utils/parseOptions.js'
6
6
 
7
7
  const HELP = `
8
8
  SYNOPSIS
9
- mediasnacks openrand [-r | --recursive]
9
+ mediasnacks openrand [-r | --recursive] [dir]
10
10
 
11
11
  DESCRIPTION
12
- Opens a random file in the current working directory
13
- `.trim()
12
+ Opens a random file in a dir on macOS.
13
+ The dir defaults to the current working directory.
14
+ `
14
15
 
15
16
  export default async function main() {
16
- if (process.platform !== 'darwin')
17
- throw new Error('Error: This command is only supported on macOS.')
18
-
19
- const { values } = await parseOptions({
17
+ const { values, positionals } = await parseOptions(HELP, {
20
18
  recursive: { short: 'r', type: 'boolean' },
21
- help: { short: 'h', type: 'boolean' }
22
19
  })
23
20
 
24
- if (values.help) {
25
- console.log(HELP)
26
- return
27
- }
21
+ if (process.platform !== 'darwin')
22
+ throw 'This command is only supported on macOS.'
28
23
 
29
- openrand('.', values.recursive)
24
+ const dir = positionals[0] || '.'
25
+ openrand(dir, values.recursive)
30
26
  }
31
27
 
32
28
  export function openrand(dir = '.', recursive = false) {
@@ -35,7 +31,7 @@ export function openrand(dir = '.', recursive = false) {
35
31
 
36
32
  export function pickRandomFile(dir = '.', recursive = false) {
37
33
  const files = readdirSync(dir, { withFileTypes: true, recursive })
38
- .filter(entry => !entry.name.startsWith('.') && entry.isFile())
39
- .map(entry => join(entry.parentPath, entry.name))
34
+ .filter(dirent => !dirent.name.startsWith('.') && dirent.isFile())
35
+ .map(dirent => join(dirent.parentPath, dirent.name))
40
36
  return files[Math.floor(Math.random() * files.length)]
41
37
  }
package/src/play.js CHANGED
@@ -8,25 +8,19 @@ SYNOPSIS
8
8
  mediasnacks play [--no-recursive] [query ...]
9
9
 
10
10
  DESCRIPTION
11
- Plays a filtered playlist with mpv.
11
+ Plays a filtered playlist with mpv
12
12
 
13
13
  EXAMPLE
14
14
  cd Music
15
15
  mediasnacks play artistX artistY
16
- `.trim()
16
+ `
17
17
 
18
18
 
19
19
  export default async function main() {
20
- const { values, positionals } = await parseOptions({
20
+ const { values, positionals } = await parseOptions(HELP, {
21
21
  recursive: { short: 'r', type: 'boolean', default: true },
22
- help: { short: 'h', type: 'boolean' }
23
22
  }, { allowNegative: true })
24
23
 
25
- if (values.help) {
26
- console.log(HELP)
27
- return
28
- }
29
-
30
24
  const files = findFiles({
31
25
  dir: '.',
32
26
  regex: new RegExp(positionals.join('|'), 'i'),
@@ -35,7 +29,7 @@ export default async function main() {
35
29
  })
36
30
 
37
31
  if (!files.length)
38
- throw new Error('No matching files found.')
32
+ throw 'No matching files found.'
39
33
 
40
34
  play(files)
41
35
  }
package/src/png.js CHANGED
@@ -11,19 +11,15 @@ DESCRIPTION
11
11
 
12
12
  EXAMPLE
13
13
  mediasnacks png *.png
14
- `.trim()
14
+ `
15
15
 
16
16
  export default async function main() {
17
- const { values, positionals } = await parseOptions({
18
- help: { short: 'h', type: 'boolean' }
19
- })
17
+ const { values, files } = await parseOptions(HELP)
20
18
 
21
- if (values.help || !positionals[0]) {
22
- console.log(HELP)
23
- return
24
- }
19
+ if (!files.length)
20
+ throw 'Missing input image(s)'
25
21
 
26
- await png(...positionals)
22
+ await png(...files)
27
23
  }
28
24
 
29
25
  export async function png(...images) {
package/src/prores.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { resolve, parse, join } from 'node:path'
2
2
  import { parseOptions } from './utils/parseOptions.js'
3
- import { assertUserHasFFmpeg, run } from './utils/subprocess.js'
3
+ import { run } from './utils/subprocess.js'
4
4
 
5
+
6
+ // https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
5
7
  export const ProresProfiles = new class {
6
- // https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
7
8
  profiles = {
8
9
  // 10-bit color depth
9
10
  0: '422 Proxy',
@@ -22,6 +23,7 @@ export const ProresProfiles = new class {
22
23
  table = () => Object.entries(this.profiles)
23
24
  }
24
25
 
26
+
25
27
  const HELP = `
26
28
  SYNOPSIS
27
29
  mediasnacks prores [options] <video>
@@ -33,7 +35,6 @@ OPTIONS
33
35
  -p, --profile <n> Default: ${ProresProfiles.default}
34
36
  -s, --start <time> In time. Unset means beginning
35
37
  -e, --end <time> Out time. Unset means end
36
- -h, --help
37
38
 
38
39
  PROFILES
39
40
  ${ProresProfiles.table().map(([num, name]) =>
@@ -46,29 +47,21 @@ TIME FORMAT
46
47
  EXAMPLES
47
48
  mediasnacks prores --end=60 video.mov // outputs video.prores.mov
48
49
  mediasnacks prores -p2 *.mov
49
- `.trim()
50
+ `
50
51
 
51
52
 
52
53
  export default async function main() {
53
- await assertUserHasFFmpeg()
54
-
55
- const { values, files } = await parseOptions({
54
+ const { values, files } = await parseOptions(HELP, {
56
55
  profile: { short: 'p', type: 'string', default: String(ProresProfiles.default) },
57
- start: { short: 's', type: 'string', default: '' },
58
- end: { short: 'e', type: 'string', default: '' },
59
- help: { short: 'h', type: 'boolean' }
56
+ start: { short: 's', type: 'string' },
57
+ end: { short: 'e', type: 'string' },
60
58
  })
61
59
 
62
- if (values.help) {
63
- console.log(HELP)
64
- return
65
- }
66
-
67
60
  if (!ProresProfiles.isValid(Number(values.profile)))
68
- throw new Error('Invalid profile. Must be one of: ' + ProresProfiles.list().join(','))
61
+ throw 'Invalid profile. Must be one of: ' + ProresProfiles.list().join(',')
69
62
 
70
63
  if (files.length !== 1)
71
- throw new Error('Expected 1 argument: video file. See mediasnacks prores --help')
64
+ throw 'Expected 1 argument: video file.'
72
65
 
73
66
  const video = resolve(files[0])
74
67
  const { name, dir } = parse(video)
package/src/qdir.js CHANGED
@@ -14,8 +14,7 @@ DESCRIPTION
14
14
  Sequentially runs all *.sh files in a folder (cwd by default).
15
15
  Completed scripts get renamed with a ".done" extension,
16
16
  or to ".failed.$exitCode"
17
- `.trim()
18
-
17
+ `
19
18
 
20
19
  function filter(f) {
21
20
  return f.endsWith('.sh')
@@ -29,19 +28,12 @@ function newExt(exitCode) {
29
28
 
30
29
 
31
30
  export default async function main() {
32
- const { values, positionals } = await parseOptions({
33
- help: { short: 'h', type: 'boolean' }
34
- })
35
-
36
- if (values.help) {
37
- console.log(HELP)
38
- return
39
- }
31
+ const { values, positionals } = await parseOptions(HELP)
40
32
 
41
33
  const dir = positionals[0] || process.cwd()
42
34
  const err = await qdir(dir)
43
35
  if (err)
44
- throw new Error(err)
36
+ throw err
45
37
  }
46
38
 
47
39
 
package/src/resize.js CHANGED
@@ -3,72 +3,58 @@ 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, assertUserHasFFmpeg } from './utils/subprocess.js'
6
+ import { ffmpeg } from './utils/subprocess.js'
7
7
  import { videoAttrs } from './utils/videoAttrs.js'
8
8
 
9
9
 
10
10
  const HELP = `
11
11
  SYNOPSIS
12
- mediasnacks resize [--width=<num>] [--height=<num>] [-y | --overwrite] [--outdir=<dir>] <files>
12
+ mediasnacks resize [options] <files>
13
13
 
14
14
  DESCRIPTION
15
15
  Resizes videos and images. The aspect ratio is preserved when only one dimension is specified.
16
16
 
17
+ OPTIONS
18
+ --width <num>
19
+ --height <num>
20
+ Only one is needed. They are -2 by default:
21
+ -1 = auto-compute while preserving the aspect ratio (may result in an odd number)
22
+ -2 = same as -1 but rounds to the nearest even number
23
+ --outdir <dir> Defaults to same dir of input file
24
+ -y, --overwrite
25
+
17
26
  EXAMPLES
18
27
  Overwrites the input file (-y)
19
28
  mediasnacks resize -y --width 480 'dir-a/**/*.png' 'dir-b/**/*.mp4'
20
29
 
21
- Output directory (-o)
30
+ Output directory
22
31
  mediasnacks resize --height 240 --outdir /tmp/out video.mov
23
-
24
- OPTIONS
25
- --width and --height are -2 by default:
26
- -1 = auto-compute while preserving the aspect ratio (may result in an odd number)
27
- -2 = same as -1 but rounds to the nearest even number
28
- `.trim()
29
-
32
+ `
30
33
 
31
34
  export default async function main() {
32
- await assertUserHasFFmpeg()
33
-
34
- const { values, files } = await parseOptions({
35
+ const { values, files } = await parseOptions(HELP, {
35
36
  width: { type: 'string', default: '-2' },
36
37
  height: { type: 'string', default: '-2' },
37
38
  outdir: { type: 'string', default: '' },
38
39
  overwrite: { short: 'y', type: 'boolean' },
39
- help: { short: 'h', type: 'boolean' },
40
40
  })
41
41
 
42
- if (values.help) {
43
- console.log(HELP)
44
- return
45
- }
46
-
47
42
  const width = Number(values.width)
48
43
  const height = Number(values.height)
49
44
 
50
- if (width <= 0 && height <= 0)
51
- throw new Error('--width or --height need to be greater than 0')
52
-
53
- if (!files.length)
54
- throw new Error('No video files specified')
55
-
56
-
57
- console.log('Resizing…')
58
- for (const file of files)
59
- try {
60
- await resize({
61
- file,
62
- outFile: join(values.outdir, file), // TODO basename ?
63
- overwrite: values.overwrite,
64
- width,
65
- height,
66
- })
67
- console.log(file)
68
- }
69
- catch (err) {
70
- console.error(err?.message || err)
71
- }
45
+ if (!files.length) throw 'No video files specified'
46
+ if (width <= 0 && height <= 0) throw '--width or --height must be > 0'
47
+
48
+ for (const file of files) {
49
+ await resize({
50
+ file,
51
+ outFile: join(values.outdir, file),
52
+ overwrite: values.overwrite,
53
+ width,
54
+ height,
55
+ })
56
+ console.log(file)
57
+ }
72
58
  }
73
59
 
74
60
 
@@ -78,10 +64,10 @@ export async function resize({ file, outFile, overwrite, width, height }) {
78
64
  || width < 0 && height === v.height
79
65
  || height < 0 && width === v.width
80
66
  )
81
- throw new Error(`no changes needed. ${file}`)
67
+ throw `no changes needed. ${file}`
82
68
 
83
69
  if (!overwrite && isFile(outFile))
84
- throw new Error(`output file exists but --overwrite=false. ${file}`)
70
+ throw `output file exists but --overwrite=false. ${file}`
85
71
 
86
72
  const tmp = uniqueFilenameFor(file)
87
73
  await ffmpeg([
package/src/seqcheck.js CHANGED
@@ -1,5 +1,6 @@
1
- import { parseArgs } from 'node:util'
2
1
  import { readdirSync } from 'node:fs'
2
+ import { parseOptions } from './utils/parseOptions.js'
3
+
3
4
 
4
5
  const LEFT_DELIM = '_'
5
6
  const RIGHT_DELIM = '.'
@@ -14,25 +15,14 @@ DESCRIPTION
14
15
  OPTIONS
15
16
  -ld, --left-delimiter <str> Delimiter before the number (default: "${LEFT_DELIM}")
16
17
  -rd, --right-delimiter <str> Delimiter after the number (default: "${RIGHT_DELIM}")
17
- -h, --help
18
- `.trim()
19
-
18
+ `
20
19
 
21
- export default function main() {
22
- const { values, positionals } = parseArgs({
23
- options: {
24
- 'left-delimiter': { type: 'string', default: LEFT_DELIM },
25
- 'right-delimiter': { type: 'string', default: RIGHT_DELIM },
26
- help: { short: 'h', type: 'boolean' },
27
- },
28
- allowPositionals: true,
20
+ export default async function main() {
21
+ const { values, positionals } = await parseOptions(HELP, {
22
+ 'left-delimiter': { type: 'string', default: LEFT_DELIM },
23
+ 'right-delimiter': { type: 'string', default: RIGHT_DELIM },
29
24
  })
30
25
 
31
- if (values.help) {
32
- console.log(HELP)
33
- return
34
- }
35
-
36
26
  const dir = positionals[0] || process.cwd()
37
27
  const missing = seqcheck(dir, values['left-delimiter'], values['right-delimiter'])
38
28
  if (missing.length)
package/src/sqcrop.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { join } from 'node:path'
2
2
  import { rename } from 'node:fs/promises'
3
3
 
4
- import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
4
+ import { ffmpeg } from './utils/subprocess.js'
5
5
  import { lstat, uniqueFilenameFor } from './utils/fs-utils.js'
6
6
  import { parseOptions } from './utils/parseOptions.js'
7
+ import { videoAttrs } from './utils/videoAttrs.js'
7
8
 
8
9
 
9
10
  const HELP = `
@@ -12,46 +13,34 @@ SYNOPSIS
12
13
 
13
14
  DESCRIPTION
14
15
  Square crops images
15
- `.trim()
16
-
16
+ `
17
17
 
18
18
  export default async function main() {
19
- await assertUserHasFFmpeg()
20
-
21
- const { values, files } = await parseOptions({
19
+ const { values, files } = await parseOptions(HELP, {
22
20
  outdir: { type: 'string', default: '' },
23
21
  overwrite: { short: 'y', type: 'boolean' },
24
- help: { short: 'h', type: 'boolean' },
25
22
  })
26
23
 
27
- if (values.help) {
28
- console.log(HELP)
29
- return
30
- }
31
-
32
24
  if (!files.length)
33
- throw new Error('No images specified. See mediasnacks sqcrop --help')
34
-
35
- console.log('Cropping…')
36
- for (const file of files)
37
- try {
38
- await sqcrop({
39
- file,
40
- outFile: join(values.outdir, file),
41
- overwrite: values.overwrite
42
- })
43
- console.log(file)
44
- }
45
- catch (err) {
46
- console.error(err?.message || err)
47
- }
25
+ throw 'No images specified'
26
+
27
+ for (const file of files) {
28
+ await sqcrop({
29
+ file,
30
+ outFile: join(values.outdir, file),
31
+ overwrite: values.overwrite
32
+ })
33
+ console.log(file)
34
+ }
48
35
  }
49
36
 
50
37
  export async function sqcrop({ file, outFile, overwrite }) {
51
38
  const stOut = lstat(outFile)
39
+ const { width, height } = await videoAttrs(file)
52
40
 
53
- if (!overwrite && stOut?.isFile()) throw new Error(`output file exists but --overwrite=false. ${file}`)
54
- if (stOut?.mtimeMs > lstat(file)?.mtimeMs) throw new Error(`outputFile is newer. ${file}`)
41
+ if (!overwrite && stOut?.isFile()) throw `output file exists. ${file}`
42
+ if (stOut?.mtimeMs > lstat(file)?.mtimeMs) throw `output file is newer. ${file}`
43
+ if (width === height) throw `already square. ${file}`
55
44
 
56
45
  const tmp = uniqueFilenameFor(file)
57
46
  await ffmpeg([
package/src/ssim.js CHANGED
@@ -8,21 +8,13 @@ SYNOPSIS
8
8
 
9
9
  DESCRIPTION
10
10
  Computes the Structural Similarity Index (SSIM) between two images using FFmpeg.
11
- `.trim()
12
-
11
+ `
13
12
 
14
13
  export default async function main() {
15
- const { values, positionals } = await parseOptions({
16
- help: { short: 'h', type: 'boolean' }
17
- })
18
-
19
- if (values.help) {
20
- console.log(HELP)
21
- return
22
- }
14
+ const { values, positionals } = await parseOptions(HELP)
23
15
 
24
16
  if (positionals.length !== 2)
25
- throw new Error('Expected two images')
17
+ throw 'Expected two images'
26
18
 
27
19
  const score = await ssim(...positionals)
28
20
  console.log(score.toString())
@@ -37,6 +29,6 @@ export async function ssim(img1, img2) {
37
29
  ])
38
30
  const match = stderr.match(/All:([\d.]+)/)
39
31
  if (!match)
40
- throw new Error(`Could not parse SSIM output:\n${stderr}`)
32
+ throw `Could not parse SSIM output:\n${stderr}`
41
33
  return parseFloat(match[1])
42
34
  }
package/src/unemoji.js CHANGED
@@ -4,6 +4,16 @@ import { dirname, basename, join } from 'node:path'
4
4
  import { parseOptions } from './utils/parseOptions.js'
5
5
  import { findFiles } from './utils/fs-utils.js'
6
6
 
7
+
8
+ const HELP = `
9
+ SYNOPSIS
10
+ mediasnacks unemoji [-r | --recursive] <dir>
11
+
12
+ DESCRIPTION
13
+ Removes emoji from filenames in the current directory.
14
+ Does not overwrite files.
15
+ `
16
+
7
17
  const EMOJI_RE = new RegExp(
8
18
  '[' +
9
19
  '\u{1F600}-\u{1F64F}' + // Emoticons
@@ -18,30 +28,13 @@ const EMOJI_RE = new RegExp(
18
28
  'gu'
19
29
  )
20
30
 
21
- const HELP = `
22
- SYNOPSIS
23
- mediasnacks unemoji [-r | --recursive] <dir>
24
-
25
- DESCRIPTION
26
- Removes emoji from filenames in the current directory.
27
- Does not overwrite files.
28
-
29
- OPTIONS
30
- -r, --recursive
31
- `.trim()
32
-
33
31
  export default async function main() {
34
- const { values, positionals } = await parseOptions({
35
- help: { short: 'h', type: 'boolean' },
32
+ const { values, positionals } = await parseOptions(HELP, {
36
33
  recursive: { short: 'r', type: 'boolean' }
37
34
  })
38
35
 
39
- if (values.help) {
40
- console.log(HELP)
41
- return
42
- }
43
-
44
- if (positionals.length !== 1) throw new Error('Only one dir is accepted')
36
+ if (positionals.length !== 1)
37
+ throw 'Must pass only one dir'
45
38
 
46
39
  const files = findFiles({
47
40
  dir: positionals[0],
@@ -49,15 +42,11 @@ export default async function main() {
49
42
  recursive: values.recursive,
50
43
  })
51
44
 
52
- for (const file of files)
53
- try {
54
- const newpath = await unemoji(file)
55
- if (newpath)
56
- console.log(`Renaming: ${file} -> ${newpath}`)
57
- }
58
- catch (err) {
59
- console.error(err?.message || err)
60
- }
45
+ for (const file of files) {
46
+ const newpath = await unemoji(file)
47
+ if (newpath)
48
+ console.log(`Renaming: ${file} -> ${newpath}`)
49
+ }
61
50
  }
62
51
 
63
52
  export async function unemoji(file) {
@@ -72,7 +61,8 @@ export async function unemoji(file) {
72
61
  return null
73
62
 
74
63
  const newpath = join(dir, newbase)
75
- if (existsSync(newpath)) throw new Error(`Skipping (exists): ${file} -> ${newpath}`)
64
+ if (existsSync(newpath))
65
+ throw `Skipping (exists): ${file} -> ${newpath}`
76
66
 
77
67
  await rename(file, newpath)
78
68
  return newpath
@@ -1,10 +1,17 @@
1
1
  import { promisify, parseArgs } from 'node:util'
2
2
  import { glob as _glob } from 'node:fs'
3
3
 
4
-
5
4
  const glob = promisify(_glob)
6
5
 
7
- export async function parseOptions(options = {}, config = {}) {
6
+
7
+ /**
8
+ * @param {string} helpText
9
+ * @param {import('node:util').ParseArgsOptionsConfig} [options]
10
+ * @param {Partial<import('node:util').ParseArgsConfig>} [config]
11
+ */
12
+ export async function parseOptions(helpText, options = {}, config = {}) {
13
+ options.help = { short: 'h', type: 'boolean' }
14
+
8
15
  const { values, positionals, tokens } = parseArgs({
9
16
  args: process.argv.slice(3),
10
17
  allowPositionals: true,
@@ -12,6 +19,12 @@ export async function parseOptions(options = {}, config = {}) {
12
19
  ...config,
13
20
  tokens: true
14
21
  })
22
+
23
+ if (values.help) {
24
+ console.log(helpText.trim())
25
+ process.exit(0)
26
+ }
27
+
15
28
  return {
16
29
  values,
17
30
  positionals,
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'node:child_process'
2
2
 
3
3
 
4
- export async function assertUserHasFFmpeg() {
4
+ async function assertUserHasFFmpeg() {
5
5
  try {
6
6
  await runSilently('ffmpeg', ['-version'])
7
7
  await runSilently('ffprobe', ['-version'])
@@ -12,6 +12,7 @@ export async function assertUserHasFFmpeg() {
12
12
  }
13
13
 
14
14
  export async function ffmpeg(args) {
15
+ await assertUserHasFFmpeg()
15
16
  return runSilently('ffmpeg', args)
16
17
  }
17
18
 
@@ -79,13 +79,13 @@ import { runSilently } from './subprocess.js'
79
79
  * @param {string} video Path to the video file.
80
80
  * @returns {Promise<VideoStream>} All video stream attributes.
81
81
  */
82
- export async function videoAttrs(v) {
82
+ export async function videoAttrs(video) {
83
83
  const { stdout } = await runSilently('ffprobe', [
84
84
  '-v', 'error',
85
85
  '-select_streams', 'v:0',
86
86
  '-show_entries', 'stream',
87
87
  '-of', 'json',
88
- v
88
+ video
89
89
  ])
90
90
  return JSON.parse(stdout).streams?.[0] || {}
91
91
  }
package/src/vsplit.js CHANGED
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs'
2
2
  import { resolve, parse, join } from 'node:path'
3
3
 
4
4
  import { parseOptions } from './utils/parseOptions.js'
5
- import { assertUserHasFFmpeg, run } from './utils/subprocess.js'
5
+ import { run } from './utils/subprocess.js'
6
6
 
7
7
 
8
8
  // TODO looks like it's missing a frame (perhaps becaue of -c copy)
@@ -31,29 +31,20 @@ EXAMPLE
31
31
 
32
32
  SEE ALSO
33
33
  mediasnacks vtrim
34
- `.trim()
34
+ `
35
35
 
36
36
 
37
37
  export default async function main() {
38
- await assertUserHasFFmpeg()
39
-
40
- const { values, files } = await parseOptions({
41
- help: { short: 'h', type: 'boolean' },
42
- })
43
-
44
- if (values.help) {
45
- console.log(HELP)
46
- return
47
- }
38
+ const { values, files } = await parseOptions(HELP)
48
39
 
49
40
  if (files.length !== 2)
50
- throw new Error('Expected 2 arguments: CSV file and video file. See mediasnacks vsplit --help')
41
+ throw 'Expected 2 arguments: CSV file and video file.'
51
42
 
52
43
  const [csvPath, videoPath] = files.map(f => resolve(f))
53
44
 
54
45
  const clips = parseCSV(csvPath)
55
46
  if (!clips.length)
56
- throw new Error('CSV file contains no clips')
47
+ throw 'CSV file contains no clips'
57
48
 
58
49
  console.log(`Splitting video into ${clips.length} clip${clips.length === 1 ? '' : 's'}…`)
59
50
  await vsplit(videoPath, clips)
@@ -64,14 +55,14 @@ function parseCSV(csvPath) {
64
55
  const lines = content.split('\n').filter(line => line.trim())
65
56
 
66
57
  if (!lines.length)
67
- throw new Error('CSV file is empty')
58
+ throw 'CSV file is empty'
68
59
 
69
60
  const clips = []
70
61
  for (let i = 1; i < lines.length; i++) { // unconditionally skips header
71
62
  const parts = lines[i].split(',').map(s => s.trim())
72
63
 
73
64
  if (parts.length !== 2)
74
- throw new Error(`Invalid CSV format at line ${i + 1}: expected 2 columns, got ${parts.length}`)
65
+ throw `Invalid CSV format at line ${i + 1}: expected 2 columns, got ${parts.length}`
75
66
 
76
67
  clips.push(parts)
77
68
  }
@@ -80,7 +71,7 @@ function parseCSV(csvPath) {
80
71
 
81
72
  export async function vsplit(videoPath, clips) {
82
73
  const { dir, name, ext } = parse(videoPath)
83
- const seqLen = Math.log10(clips.length) + 1 | 0
74
+ const seqLen = String(clips.length).length
84
75
 
85
76
  for (let i = 0; i < clips.length; i++) {
86
77
  const [start, end] = clips[i]