mediasnacks 0.25.0 → 0.26.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/README.md CHANGED
@@ -47,7 +47,7 @@ mediasnacks <command> <args>
47
47
  - `flattendir`: Moves unique files to the top dir and deletes empty dirs
48
48
  - `qdir` Sequentially runs all *.sh files in a folder
49
49
  - `seqcheck` Finds missing sequence number
50
- - `random` Opens a random file
50
+ - `openrand` Opens a random file
51
51
  - `play` Plays filtered playlist with mpv
52
52
 
53
53
 
package/index.js ADDED
@@ -0,0 +1,18 @@
1
+ export { avif } from './src/avif.js'
2
+ export { countframes } from './src/countframes.js'
3
+ export { detectdups } from './src/detectdups.js'
4
+ export { dropdups } from './src/dropdups.js'
5
+ export { edgespic } from './src/edgespic.js'
6
+ export { frameseq } from './src/frameseq.js'
7
+ export { hev1tohvc1 } from './src/hev1tohvc1.js'
8
+ export { moov2front } from './src/moov2front.js'
9
+ export { play } from './src/play.js'
10
+ export { prores } from './src/prores.js'
11
+ export { qdir } from './src/qdir.js'
12
+ export { openrand, pickRandomFile } from './src/openrand.js'
13
+ export { resize } from './src/resize.js'
14
+ export { seqcheck } from './src/seqcheck.js'
15
+ export { sqcrop } from './src/sqcrop.js'
16
+ export { ssim } from './src/ssim.js'
17
+ export { vsplit } from './src/vsplit.js'
18
+ export { vtrim } from './src/vtrim.js'
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
- "name": "mediasnacks",
3
- "version": "0.25.0",
4
- "description": "Utilities for optimizing and preparing videos and images",
5
- "license": "MIT",
6
- "author": "Eric Fortis",
7
- "type": "module",
2
+ "name": "mediasnacks",
3
+ "version": "0.26.0",
4
+ "description": "Utilities for optimizing and preparing videos and images",
5
+ "license": "MIT",
6
+ "author": "Eric Fortis",
7
+ "type": "module",
8
8
  "bin": {
9
9
  "mediasnacks": "src/cli.js"
10
10
  },
@@ -14,8 +14,16 @@
14
14
  "postinstall": "node install-zsh-completions.js",
15
15
  "dev-install": "npm i -g . --ignore-scripts=false"
16
16
  },
17
+ "exports": {
18
+ ".": {
19
+ "import": "./index.js",
20
+ "types": "./index.d.ts"
21
+ }
22
+ },
17
23
  "files": [
18
24
  "src",
25
+ "index.js",
26
+ "index.d.js",
19
27
  "install-zsh-completions.js"
20
28
  ]
21
29
  }
package/src/avif.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { join, basename, dirname } from 'node:path'
3
2
  import { parseOptions } from './utils/parseOptions.js'
4
3
  import { replaceExt, lstat } from './utils/fs-utils.js'
@@ -7,22 +6,22 @@ import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
7
6
 
8
7
  const HELP = `
9
8
  SYNOPSIS
10
- mediasnacks avif [-y | --overwrite] [--output-dir=<dir>] <images>
9
+ mediasnacks avif [-y | --overwrite] [--outdir=<dir>] <images>
11
10
 
12
11
  DESCRIPTION
13
12
  Converts images to AVIF.
14
13
 
15
14
  EXAMPLES
16
15
  mediasnacks avif -y '*.png'
17
- mediasnacks avif --output-dir=foo/ 'a/**/*.png'
16
+ mediasnacks avif --outdir=foo/ 'a/**/*.png'
18
17
  `.trim()
19
18
 
20
19
 
21
- async function main() {
20
+ export default async function main() {
22
21
  await assertUserHasFFmpeg()
23
22
 
24
23
  const { values, files } = await parseOptions({
25
- 'output-dir': { type: 'string', default: '' },
24
+ outdir: { type: 'string', default: '' },
26
25
  overwrite: { short: 'y', type: 'boolean' },
27
26
  help: { short: 'h', type: 'boolean' },
28
27
  })
@@ -37,27 +36,24 @@ async function main() {
37
36
 
38
37
  console.log('AVIF…')
39
38
  for (const file of files)
40
- await avif({
41
- file,
42
- outFile: join(values['output-dir'] || dirname(file), replaceExt(basename(file), 'avif')),
43
- overwrite: values.overwrite
44
- })
39
+ try {
40
+ await avif({
41
+ file,
42
+ outFile: join(values.outdir || dirname(file), replaceExt(basename(file), 'avif')),
43
+ overwrite: values.overwrite
44
+ })
45
+ console.log(file)
46
+ }
47
+ catch (err) {
48
+ console.error(err?.message || err)
49
+ }
45
50
  }
46
51
 
47
- async function avif({ file, outFile, overwrite }) {
52
+ export async function avif({ file, outFile, overwrite = false }) {
48
53
  const stAvif = lstat(outFile)
54
+ if (!overwrite && stAvif?.isFile()) throw new Error(`output file exists but --overwrite=false. ${file}`)
55
+ if (stAvif?.mtimeMs > lstat(file)?.mtimeMs) throw new Error(`avif is newer. ${file}`)
49
56
 
50
- if (!overwrite && stAvif?.isFile()) {
51
- console.log('(skipped: output file exists but --overwrite=false)', file)
52
- return
53
- }
54
- if (stAvif?.mtimeMs > lstat(file)?.mtimeMs) {
55
- console.log('(skipped: avif is newer)', file)
56
- return
57
- }
58
-
59
- // TODO fix transparent PNGs
60
- console.log(file)
61
57
  await ffmpeg([
62
58
  '-y',
63
59
  '-i', file,
@@ -66,8 +62,3 @@ async function avif({ file, outFile, overwrite }) {
66
62
  outFile
67
63
  ])
68
64
  }
69
-
70
- main().catch(err => {
71
- console.error(err.message)
72
- process.exit(1)
73
- })
package/src/cli.js CHANGED
@@ -1,45 +1,45 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { join } from 'node:path'
4
- import { styleText } from 'node:util'
5
4
  import { spawn } from 'node:child_process'
5
+ import { styleText } from 'node:util'
6
6
  import pkgJSON from '../package.json' with { type: 'json' }
7
7
 
8
8
 
9
9
  const COMMANDS = {
10
- avif: ['avif.js', 'Converts images to AVIF'],
11
- png: ['png.sh', 'Optimizes PNG images with oxipng'],
12
- sqcrop: ['sqcrop.js', 'Square crops images\n'],
13
-
14
- resize: ['resize.js', 'Resizes videos or images'],
15
- edgespic: ['edgespic.js', 'Extracts first and last frames'],
16
- frameseq: ['frameseq.js', 'Converts video to sequence of PNGs'],
17
- countframes: ['countframes.js', 'Counts frames in a video'],
18
- ssim: ['ssim.js', 'Computes SSIM between two images'],
19
- gif: ['gif.sh', 'Video to GIF\n'],
20
-
21
- detectdups: ['detectdups.js', 'Detects duplicate frames in a video'],
22
- dropdups: ['dropdups.js', 'Removes duplicate frames in a video'],
23
- framediff: ['framediff.sh', 'Plays a video of adjacent frames diff'],
24
- hev1tohvc1: ['hev1tohvc1.js', 'Fixes video thumbnails not rendering on macOS Finder'],
25
- moov2front: ['moov2front.js', 'Rearranges .mov and .mp4 metadata for fast-start streaming'],
26
- vconcat: ['vconcat.sh', 'Concatenates videos'],
27
- vdiff: ['vdiff.sh', 'Plays a video with the difference of two videos'],
28
- vsplit: ['vsplit.js', 'Splits a video into multiple clips from CSV timestamps'],
29
- vtrim: ['vtrim.js', 'Trims video from start to end time'],
30
- prores: ['prores.js', 'Converts video to ProRes\n'],
31
-
32
- flattendir: ['flattendir.sh', 'Moves all files to top dir and deletes dirs'],
33
- qdir: ['qdir.js', 'Sequentially runs all *.sh files in a folder'],
34
- seqcheck: ['seqcheck.js', 'Finds missing sequence number'],
35
- random: ['random.js', 'Opens a random file (macOS only)'],
36
- play: ['play.js', 'Plays filtered playlist with mpv\n'],
37
-
38
- dlaudio: ['dlaudio.sh', 'yt-dlp best audio'],
39
- dlvideo: ['dlvideo.sh', 'yt-dlp best video\n'],
40
-
41
- unemoji: ['unemoji.sh', 'Removes emojis from filenames'],
42
- rmcover: ['rmcover.sh', 'Removes cover art'],
10
+ avif: ['./avif.js', 'Converts images to AVIF'],
11
+ png: ['./png.sh', 'Optimizes PNG images with oxipng'],
12
+ sqcrop: ['./sqcrop.js', 'Square crops images\n'],
13
+
14
+ resize: ['./resize.js', 'Resizes videos or images'],
15
+ edgespic: ['./edgespic.js', 'Extracts first and last frames'],
16
+ frameseq: ['./frameseq.js', 'Converts video to sequence of PNGs'],
17
+ countframes: ['./countframes.js', 'Counts frames in a video'],
18
+ ssim: ['./ssim.js', 'Computes SSIM between two images'],
19
+ gif: ['./gif.sh', 'Video to GIF\n'],
20
+
21
+ detectdups: ['./detectdups.js', 'Detects duplicate frames in a video'],
22
+ dropdups: ['./dropdups.js', 'Removes duplicate frames in a video'],
23
+ framediff: ['./framediff.sh', 'Plays a video of adjacent frames diff'],
24
+ hev1tohvc1: ['./hev1tohvc1.js', 'Fixes video thumbnails not rendering on macOS Finder'],
25
+ moov2front: ['./moov2front.js', 'Rearranges .mov and .mp4 metadata for fast-start streaming'],
26
+ vconcat: ['./vconcat.sh', 'Concatenates videos'],
27
+ vdiff: ['./vdiff.sh', 'Plays a video with the difference of two videos'],
28
+ vsplit: ['./vsplit.js', 'Splits a video into multiple clips from CSV timestamps'],
29
+ vtrim: ['./vtrim.js', 'Trims video from start to end time'],
30
+ prores: ['./prores.js', 'Converts video to ProRes\n'],
31
+
32
+ flattendir: ['./flattendir.sh', 'Moves all files to top dir and deletes dirs'],
33
+ qdir: ['./qdir.js', 'Sequentially runs all *.sh files in a folder'],
34
+ seqcheck: ['./seqcheck.js', 'Finds missing sequence number'],
35
+ openrand: ['./openrand.js', 'Opens a random file (macOS only)'],
36
+ play: ['./play.js', 'Plays filtered playlist with mpv\n'],
37
+
38
+ dlaudio: ['./dlaudio.sh', 'yt-dlp best audio'],
39
+ dlvideo: ['./dlvideo.sh', 'yt-dlp best video\n'],
40
+
41
+ unemoji: ['./unemoji.sh', 'Removes emojis from filenames'],
42
+ rmcover: ['./rmcover.sh', 'Removes cover art'],
43
43
  }
44
44
 
45
45
  export function commandsSummary() {
@@ -57,7 +57,7 @@ ${commandsSummary().map(([cmd, desc]) =>
57
57
  `.trim()
58
58
 
59
59
 
60
- function main() {
60
+ async function main() {
61
61
  const [, , opt, ...args] = process.argv
62
62
 
63
63
  if (opt === '-v' || opt === '--version') {
@@ -78,10 +78,16 @@ function main() {
78
78
  process.exit(1)
79
79
  }
80
80
 
81
- const cmd = join(import.meta.dirname, COMMANDS[opt][0])
82
- spawn(cmd, args, { stdio: 'inherit' })
83
- .on('exit', process.exit)
81
+ const cmd = COMMANDS[opt][0]
82
+ if (cmd.endsWith('.js'))
83
+ (await import(cmd)).default()
84
+ else
85
+ spawn(join(import.meta.dirname, cmd), args, { stdio: 'inherit' })
86
+ .on('exit', process.exit)
84
87
  }
85
88
 
86
89
  if (import.meta.main)
87
- main()
90
+ main().catch(err => {
91
+ console.error(err?.message || err)
92
+ process.exit(1)
93
+ })
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  import { parseOptions } from './utils/parseOptions.js'
4
2
  import { assertUserHasFFmpeg } from './utils/subprocess.js'
5
3
  import { videoAttrs } from './utils/videoAttrs.js'
@@ -24,7 +22,7 @@ EXAMPLES
24
22
  `.trim()
25
23
 
26
24
 
27
- async function main() {
25
+ export default async function main() {
28
26
  await assertUserHasFFmpeg()
29
27
 
30
28
  const { values, files } = await parseOptions({
@@ -40,16 +38,16 @@ async function main() {
40
38
  }
41
39
 
42
40
  const { fps, start, end } = values
43
- const file = files[0]
44
- if (!file) throw new Error('No video file specified')
41
+ const video = files[0]
42
+ if (!video) throw new Error('No video file specified')
45
43
 
46
- const n = await countframes(file, fps, start, end)
44
+ const n = await countframes({ video, fps, start, end })
47
45
  console.log(String(n))
48
46
  }
49
47
 
50
48
 
51
- export async function countframes(file, fps, start, end) {
52
- const v = await videoAttrs(file)
49
+ export async function countframes({ video, fps, start, end }) {
50
+ const v = await videoAttrs(video)
53
51
  const videoDuration = parseFloat(v.duration || 0)
54
52
  const startSecs = start ? parseTimecode(start) : 0
55
53
  const endSecs = end ? parseTimecode(end) : videoDuration
@@ -57,10 +55,3 @@ export async function countframes(file, fps, start, end) {
57
55
  const actualFps = fps ? Number(fps) : eval(v.r_frame_rate)
58
56
  return Math.ceil(durationLimit * actualFps)
59
57
  }
60
-
61
-
62
- if (import.meta.main)
63
- main().catch(err => {
64
- console.error(err.message)
65
- process.exit(1)
66
- })
package/src/detectdups.js CHANGED
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  import { parseOptions } from './utils/parseOptions.js'
4
2
  import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
5
3
  import { videoAttrs } from './utils/videoAttrs.js'
@@ -31,7 +29,7 @@ SEE ALSO
31
29
  `.trim()
32
30
 
33
31
 
34
- async function main() {
32
+ export default async function main() {
35
33
  await assertUserHasFFmpeg()
36
34
 
37
35
  const { values, files } = await parseOptions({
@@ -45,13 +43,11 @@ async function main() {
45
43
  return
46
44
  }
47
45
 
48
- if (files.length !== 1)
49
- throw new Error('Invalid input file. One video file must be specified. See mediasnacks detectdups --help')
50
-
51
- const v = await videoAttrs(files[0])
46
+ if (files.length !== 1) throw new Error('Invalid input file. One video file must be specified. See mediasnacks detectdups --help')
52
47
 
53
- if (v.codec_type !== 'video')
54
- throw new Error('Invalid input file. Must be a video.')
48
+ const video = files[0]
49
+ const v = await videoAttrs(video)
50
+ if (v.codec_type !== 'video') throw new Error('Invalid input file. Must be a video.')
55
51
 
56
52
  const vDur = Number(v.duration)
57
53
 
@@ -67,7 +63,7 @@ async function main() {
67
63
  if (isNaN(duration) || duration < 1) throw new Error(`Invalid --duration value: ${values.duration}`)
68
64
  if ((seek + duration) > vDur) throw new Error(`Invalid analysis range. Exceeds video duration: ${vDur}`)
69
65
 
70
- const dups = await detectdups(files[0], seek, duration)
66
+ const dups = await detectdups({ video: files[0], seek, duration })
71
67
  const h = deltaHistogram(dups)
72
68
  const report = {
73
69
  n: maxFreqKey(h),
@@ -80,12 +76,12 @@ async function main() {
80
76
  console.log(JSON.stringify(report, null, 2))
81
77
  }
82
78
 
83
- export async function detectdups(video, seek, duration) {
79
+ export async function detectdups({ video, seek, duration }) {
84
80
  const { stderr } = await ffmpeg([
85
81
  '-v', 'info',
86
82
  '-stats',
87
- '-ss', seek,
88
- '-t', duration,
83
+ seek ? ['-ss', seek] : [],
84
+ duration ? ['-t', duration] : [],
89
85
  '-i', video,
90
86
  '-vf', [
91
87
  'tblend=all_mode=difference',
@@ -93,7 +89,7 @@ export async function detectdups(video, seek, duration) {
93
89
  'showinfo',
94
90
  ].join(','),
95
91
  '-f', 'null', '-',
96
- ])
92
+ ].flat())
97
93
 
98
94
  const reNearBlackFrames = /n:\s*(\d+).*?mean:\[0].*?stdev:\[([0-9.]+)]/
99
95
  const dupFrames = []
@@ -133,10 +129,3 @@ function maxFreqKey(histogram) {
133
129
  ? Number(maxKey)
134
130
  : null
135
131
  }
136
-
137
-
138
- if (import.meta.main)
139
- main().catch(err => {
140
- console.error(err.message || err)
141
- process.exit(1)
142
- })
package/src/dropdups.js CHANGED
@@ -1,7 +1,4 @@
1
- #!/usr/bin/env node
2
-
3
1
  import { resolve, parse, format } from 'node:path'
4
-
5
2
  import { parseOptions } from './utils/parseOptions.js'
6
3
  import { ffmpeg, assertUserHasFFmpeg, run } from './utils/subprocess.js'
7
4
  import { ProresProfiles } from './prores.js'
@@ -30,7 +27,7 @@ EXAMPLES
30
27
  `.trim()
31
28
 
32
29
 
33
- async function main() {
30
+ export default async function main() {
34
31
  await assertUserHasFFmpeg()
35
32
 
36
33
  const { values, files } = await parseOptions({
@@ -55,7 +52,7 @@ async function main() {
55
52
  await dropdups(resolve(file), dupFrameNum)
56
53
  }
57
54
 
58
- async function dropdups(video, dupFrameNum) {
55
+ export async function dropdups(video, dupFrameNum) {
59
56
  await run('ffmpeg', [
60
57
  '-v', 'error',
61
58
  '-stats',
@@ -77,8 +74,3 @@ function makeOutputPath(video) {
77
74
  ? format({ dir, name: `${name}.dedup`, ext: '.mov' })
78
75
  : format({ dir, name, ext: '.mov' })
79
76
  }
80
-
81
- main().catch(err => {
82
- console.error(err.message || err)
83
- process.exit(1)
84
- })
package/src/edgespic.js CHANGED
@@ -1,11 +1,9 @@
1
- #!/usr/bin/env node
2
-
3
1
  import { basename, extname, join, parse } from 'node:path'
4
2
 
5
3
  import { mkDir } from './utils/fs-utils.js'
4
+ import { videoAttrs } from './utils/videoAttrs.js'
6
5
  import { parseOptions } from './utils/parseOptions.js'
7
6
  import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
8
- import { videoAttrs } from './utils/videoAttrs.js'
9
7
 
10
8
 
11
9
  const HELP = `
@@ -24,7 +22,7 @@ EXAMPLES
24
22
  `.trim()
25
23
 
26
24
 
27
- async function main() {
25
+ export default async function main() {
28
26
  await assertUserHasFFmpeg()
29
27
 
30
28
  const { values, files } = await parseOptions({
@@ -38,14 +36,11 @@ async function main() {
38
36
  }
39
37
 
40
38
  const width = Number(values['width'])
41
- if (width <= 0 || !Number.isInteger(width))
42
- throw new Error('--width must be a positive number')
43
-
44
- if (!files.length)
45
- throw new Error('No video files specified')
39
+ if (width <= 0 || !Number.isInteger(width)) throw new Error('--width must be a positive number')
40
+ if (!files.length) throw new Error('No video files specified')
46
41
 
47
42
  const outDir = join(parse(files[0]).dir, 'edgespic')
48
- await mkDir(outDir)
43
+ await mkDir(outDir)
49
44
 
50
45
  console.log('Extracting edge frames…')
51
46
  for (const file of files)
@@ -53,7 +48,7 @@ async function main() {
53
48
  }
54
49
 
55
50
 
56
- async function edgespic(video, width, outDir) {
51
+ export async function edgespic(video, width, outDir) {
57
52
  const { r_frame_rate } = await videoAttrs(video)
58
53
  const name = basename(video, extname(video))
59
54
 
@@ -74,9 +69,3 @@ async function edgespic(video, width, outDir) {
74
69
  join(outDir, `${name}_last.png`)
75
70
  ])
76
71
  }
77
-
78
-
79
- main().catch(err => {
80
- console.error(err.message)
81
- process.exit(1)
82
- })
package/src/frameseq.js CHANGED
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  import { basename, extname, join, parse } from 'node:path'
4
2
 
5
3
  import { mkDir } from './utils/fs-utils.js'
@@ -29,13 +27,14 @@ EXAMPLES
29
27
  `.trim()
30
28
 
31
29
 
32
- async function main() {
30
+ export default async function main() {
33
31
  await assertUserHasFFmpeg()
34
32
 
35
33
  const { values, files } = await parseOptions({
36
- fps: { type: 'string', default: '' },
34
+ fps: { short: 'f', type: 'string', default: '' },
37
35
  start: { short: 's', type: 'string', default: '' },
38
36
  end: { short: 'e', type: 'string', default: '' },
37
+ outdir: { type: 'string', default: '' },
39
38
  help: { short: 'h', type: 'boolean' }
40
39
  })
41
40
 
@@ -44,21 +43,21 @@ async function main() {
44
43
  return
45
44
  }
46
45
 
47
- const { fps, start, end } = values
48
- const file = files[0]
49
- if (!file) throw new Error('No video files specified')
46
+ const { fps, start, end, outdir } = values
47
+ const video = files[0]
48
+ if (!video) throw new Error('No video files specified')
50
49
  if (fps && isNaN(parseFloat(fps))) throw new Error('Invalid --fps')
51
50
  if (start && isNaN(parseFloat(start))) throw new Error('Invalid --start')
52
51
  if (end && isNaN(parseFloat(end))) throw new Error('Invalid --end')
53
52
 
54
- const nFrames = await countframes(file, fps, start, end)
53
+ const nFrames = await countframes({ video, fps, start, end })
55
54
  const pad = String(nFrames).length
56
- await frameseq(file, fps, start, end, pad)
55
+ await frameseq({ video, fps, start, end, pad, outdir })
57
56
  }
58
57
 
59
- async function frameseq(video, fps, start, end, pad) {
58
+ export async function frameseq({ video, fps, start, end, pad, outdir }) {
60
59
  const name = basename(video, extname(video))
61
- const outDir = join(parse(video).dir, name)
60
+ const outDir = outdir || join(parse(video).dir, name)
62
61
  await mkDir(outDir)
63
62
  await ffmpeg([
64
63
  start ? ['-ss', start] : [],
@@ -68,8 +67,3 @@ async function frameseq(video, fps, start, end, pad) {
68
67
  join(outDir, `${name}_%0${pad}d.png`)
69
68
  ].flat())
70
69
  }
71
-
72
- main().catch(err => {
73
- console.error(err.message)
74
- process.exit(1)
75
- })
package/src/hev1tohvc1.js CHANGED
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  import { parseOptions } from './utils/parseOptions.js'
4
2
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
5
3
  import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
@@ -17,7 +15,7 @@ DESCRIPTION
17
15
  `.trim()
18
16
 
19
17
 
20
- async function main() {
18
+ export default async function main() {
21
19
  await assertUserHasFFmpeg()
22
20
 
23
21
  const { values, files } = await parseOptions({
@@ -29,21 +27,22 @@ async function main() {
29
27
  return
30
28
  }
31
29
 
32
- if (!files.length)
33
- throw new Error(HELP)
30
+ if (!files.length) throw new Error('Missing input file(s)')
34
31
 
35
32
  for (const file of files)
36
- await hev1tohvc1(file)
33
+ try {
34
+ await hev1tohvc1(file)
35
+ console.log(file)
36
+ }
37
+ catch (err) {
38
+ console.error(err?.message || err)
39
+ }
37
40
  }
38
41
 
39
- async function hev1tohvc1(file) {
42
+ export async function hev1tohvc1(file) {
40
43
  const v = await videoAttrs(file)
41
- if (v.codec_tag_string !== 'hev1') {
42
- console.log('(skipped: non hev1)', file)
43
- return
44
- }
44
+ if (v.codec_tag_string !== 'hev1') throw new Error(`non hev1 ${file}`)
45
45
 
46
- console.log(file)
47
46
  const tmp = uniqueFilenameFor(file)
48
47
  await ffmpeg([
49
48
  '-i', file,
@@ -53,9 +52,3 @@ async function hev1tohvc1(file) {
53
52
  ])
54
53
  await overwrite(tmp, file)
55
54
  }
56
-
57
-
58
- main().catch(err => {
59
- console.error(err.message)
60
- process.exit(1)
61
- })
package/src/moov2front.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
3
2
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
4
3
  import { parseOptions } from './utils/parseOptions.js'
@@ -20,7 +19,7 @@ NOTES
20
19
  `.trim()
21
20
 
22
21
 
23
- async function main() {
22
+ export default async function main() {
24
23
  await assertUserHasFFmpeg()
25
24
 
26
25
  const { values, files } = await parseOptions({
@@ -32,25 +31,23 @@ async function main() {
32
31
  return
33
32
  }
34
33
 
35
- if (!files.length)
36
- throw new Error(HELP)
34
+ if (!files.length) throw new Error('Missing input file(s)')
37
35
 
38
36
  console.log('Optimizing video for progressive download…')
39
37
  for (const file of files)
40
- await moov2front(file)
38
+ try {
39
+ await moov2front(file)
40
+ console.log(file)
41
+ }
42
+ catch (err) {
43
+ console.error(err?.message || err)
44
+ }
41
45
  }
42
46
 
43
- async function moov2front(file) {
44
- if (!/\.(mp4|mov)$/i.test(file)) {
45
- console.log('(skipped: not mp4/mov)', file)
46
- return
47
- }
48
- if (await moovIsBeforeMdat(file)) {
49
- console.log('(skipped: no changes needed)', file)
50
- return
51
- }
47
+ export async function moov2front(file) {
48
+ if (!/\.(mp4|mov)$/i.test(file)) throw new Error(`not mp4/mov. ${file}`)
49
+ if (await moovIsBeforeMdat(file)) throw new Error(`no changes needed. ${file}`)
52
50
 
53
- console.log(file)
54
51
  const tmp = uniqueFilenameFor(file)
55
52
  await ffmpeg([
56
53
  '-hide_banner',
@@ -71,9 +68,3 @@ async function moovIsBeforeMdat(file) {
71
68
  const firstMatchedAtom = stderr.match(/type:'(moov|mdat)'/)?.[1]
72
69
  return firstMatchedAtom === 'moov'
73
70
  }
74
-
75
-
76
- main().catch(err => {
77
- console.error(err.message)
78
- process.exit(1)
79
- })
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { join } from 'node:path'
3
2
  import { spawn } from 'node:child_process'
4
3
  import { readdirSync } from 'node:fs'
@@ -7,13 +6,13 @@ import { parseOptions } from './utils/parseOptions.js'
7
6
 
8
7
  const HELP = `
9
8
  SYNOPSIS
10
- mediasnacks random [-r | --recursive]
9
+ mediasnacks openrand [-r | --recursive]
11
10
 
12
11
  DESCRIPTION
13
12
  Opens a random file in the current working directory
14
13
  `.trim()
15
14
 
16
- async function main() {
15
+ export default async function main() {
17
16
  if (process.platform !== 'darwin')
18
17
  throw new Error('Error: This command is only supported on macOS.')
19
18
 
@@ -27,17 +26,16 @@ async function main() {
27
26
  return
28
27
  }
29
28
 
30
- spawn('open', [pickRandomFile('.', values.recursive)])
29
+ openrand('.', values.recursive)
31
30
  }
32
31
 
33
- function pickRandomFile(dir, recursive) {
32
+ export function openrand(dir = '.', recursive = false) {
33
+ spawn('open', [pickRandomFile(dir, recursive)])
34
+ }
35
+
36
+ export function pickRandomFile(dir = '.', recursive = false) {
34
37
  const files = readdirSync(dir, { withFileTypes: true, recursive })
35
38
  .filter(entry => !entry.name.startsWith('.') && entry.isFile())
36
39
  .map(entry => join(entry.parentPath, entry.name))
37
40
  return files[Math.floor(Math.random() * files.length)]
38
41
  }
39
-
40
- main().catch(err => {
41
- console.error(err.message)
42
- process.exit(1)
43
- })
package/src/play.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { spawn } from 'node:child_process'
3
2
  import { parseOptions } from './utils/parseOptions.js'
4
3
  import { findFiles } from './utils/fs-utils.js'
@@ -17,7 +16,7 @@ EXAMPLE
17
16
  `.trim()
18
17
 
19
18
 
20
- async function main() {
19
+ export default async function main() {
21
20
  const { values, positionals } = await parseOptions({
22
21
  recursive: { short: 'r', type: 'boolean', default: true },
23
22
  help: { short: 'h', type: 'boolean' }
@@ -41,7 +40,7 @@ async function main() {
41
40
  play(files)
42
41
  }
43
42
 
44
- function play(files) {
43
+ export function play(files) {
45
44
  const mpv = spawn('mpv', ['--playlist=-'], {
46
45
  detached: true,
47
46
  stdio: ['pipe', 'ignore', 'ignore']
@@ -57,8 +56,3 @@ function play(files) {
57
56
  process.exit(1)
58
57
  })
59
58
  }
60
-
61
- main().catch(err => {
62
- console.error(err.message)
63
- process.exit(1)
64
- })
package/src/prores.js CHANGED
@@ -1,10 +1,9 @@
1
- #!/usr/bin/env node
2
1
  import { resolve, parse, join } from 'node:path'
3
2
  import { parseOptions } from './utils/parseOptions.js'
4
3
  import { assertUserHasFFmpeg, run } from './utils/subprocess.js'
5
4
 
6
5
  export const ProresProfiles = new class {
7
- // https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
6
+ // https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
8
7
  profiles = {
9
8
  // 10-bit color depth
10
9
  0: '422 Proxy',
@@ -50,7 +49,7 @@ EXAMPLES
50
49
  `.trim()
51
50
 
52
51
 
53
- async function main() {
52
+ export default async function main() {
54
53
  await assertUserHasFFmpeg()
55
54
 
56
55
  const { values, files } = await parseOptions({
@@ -75,10 +74,11 @@ async function main() {
75
74
  const { name, dir } = parse(video)
76
75
  const output = join(dir, `${name}.prores.mov`)
77
76
 
78
- await prores(video, values.start, values.end, values.profile, output)
77
+ const { profile, start, end } = values
78
+ await prores({ video, profile, start, end, output })
79
79
  }
80
80
 
81
- async function prores(video, start, end, profile, output) {
81
+ export async function prores({ video, profile, start, end, output }) {
82
82
  await run('ffmpeg', [
83
83
  '-v', 'error',
84
84
  '-stats',
@@ -89,9 +89,3 @@ async function prores(video, start, end, profile, output) {
89
89
  output
90
90
  ].flat())
91
91
  }
92
-
93
- if (import.meta.main)
94
- main().catch(err => {
95
- console.error(err.message || err)
96
- process.exit(1)
97
- })
package/src/qdir.js CHANGED
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  import { join } from 'node:path'
4
2
  import { spawn } from 'node:child_process'
5
3
  import { readdir, writeFile, unlink, rename } from 'node:fs/promises'
@@ -30,7 +28,7 @@ function newExt(exitCode) {
30
28
  }
31
29
 
32
30
 
33
- async function main() {
31
+ export default async function main() {
34
32
  const { values, positionals } = await parseOptions({
35
33
  help: { short: 'h', type: 'boolean' }
36
34
  })
@@ -98,10 +96,3 @@ async function runShell(scriptPath) {
98
96
  function sleep(ms) {
99
97
  return new Promise(resolve => setTimeout(resolve, ms))
100
98
  }
101
-
102
-
103
- if (import.meta.main)
104
- main().catch(err => {
105
- console.error(err.message || err)
106
- process.exit(1)
107
- })
package/src/resize.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { join } from 'node:path'
3
2
  import { rename } from 'node:fs/promises'
4
3
 
@@ -10,7 +9,7 @@ import { videoAttrs } from './utils/videoAttrs.js'
10
9
 
11
10
  const HELP = `
12
11
  SYNOPSIS
13
- mediasnacks resize [--width=<num>] [--height=<num>] [-y | --overwrite] [--output-dir=<dir>] <files>
12
+ mediasnacks resize [--width=<num>] [--height=<num>] [-y | --overwrite] [--outdir=<dir>] <files>
14
13
 
15
14
  DESCRIPTION
16
15
  Resizes videos and images. The aspect ratio is preserved when only one dimension is specified.
@@ -20,7 +19,7 @@ EXAMPLES
20
19
  mediasnacks resize -y --width 480 'dir-a/**/*.png' 'dir-b/**/*.mp4'
21
20
 
22
21
  Output directory (-o)
23
- mediasnacks resize --height 240 --output-dir /tmp/out video.mov
22
+ mediasnacks resize --height 240 --outdir /tmp/out video.mov
24
23
 
25
24
  OPTIONS
26
25
  --width and --height are -2 by default:
@@ -29,13 +28,13 @@ OPTIONS
29
28
  `.trim()
30
29
 
31
30
 
32
- async function main() {
31
+ export default async function main() {
33
32
  await assertUserHasFFmpeg()
34
33
 
35
34
  const { values, files } = await parseOptions({
36
35
  width: { type: 'string', default: '-2' },
37
36
  height: { type: 'string', default: '-2' },
38
- 'output-dir': { type: 'string', default: '' },
37
+ outdir: { type: 'string', default: '' },
39
38
  overwrite: { short: 'y', type: 'boolean' },
40
39
  help: { short: 'h', type: 'boolean' },
41
40
  })
@@ -57,31 +56,33 @@ async function main() {
57
56
 
58
57
  console.log('Resizing…')
59
58
  for (const file of files)
60
- await resize({
61
- file,
62
- outFile: join(values['output-dir'], file), // TODO basename ?
63
- overwrite: values.overwrite,
64
- width,
65
- height,
66
- })
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
+ }
67
72
  }
68
73
 
69
74
 
70
- async function resize({ file, outFile, overwrite, width, height }) {
75
+ export async function resize({ file, outFile, overwrite, width, height }) {
71
76
  const v = await videoAttrs(file)
72
77
  if (width === v.width && height === v.height
73
78
  || width < 0 && height === v.height
74
- || height < 0 && width === v.width) {
75
- console.log('(skipped: no changes needed)', file)
76
- return
77
- }
79
+ || height < 0 && width === v.width
80
+ )
81
+ throw new Error(`no changes needed. ${file}`)
78
82
 
79
- if (!overwrite && isFile(outFile)) {
80
- console.log('(skipped: output file exists but --overwrite=false)', file)
81
- return
82
- }
83
+ if (!overwrite && isFile(outFile))
84
+ throw new Error(`output file exists but --overwrite=false. ${file}`)
83
85
 
84
- console.log(file)
85
86
  const tmp = uniqueFilenameFor(file)
86
87
  await ffmpeg([
87
88
  '-i', file,
@@ -91,9 +92,3 @@ async function resize({ file, outFile, overwrite, width, height }) {
91
92
  ])
92
93
  await rename(tmp, outFile)
93
94
  }
94
-
95
-
96
- main().catch(err => {
97
- console.error(err.message)
98
- process.exit(1)
99
- })
package/src/seqcheck.js CHANGED
@@ -1,7 +1,8 @@
1
- #!/usr/bin/env node
2
1
  import { parseArgs } from 'node:util'
3
2
  import { readdirSync } from 'node:fs'
4
3
 
4
+ const LEFT_DELIM = '_'
5
+ const RIGHT_DELIM = '.'
5
6
 
6
7
  const HELP = `
7
8
  SYNOPSIS
@@ -11,17 +12,17 @@ DESCRIPTION
11
12
  Find missing numbered files in a sequence.
12
13
 
13
14
  OPTIONS
14
- -ld, --left-delimiter <str> Delimiter before the number (default: "_")
15
- -rd, --right-delimiter <str> Delimiter after the number (default: ".")
15
+ -ld, --left-delimiter <str> Delimiter before the number (default: "${LEFT_DELIM}")
16
+ -rd, --right-delimiter <str> Delimiter after the number (default: "${RIGHT_DELIM}")
16
17
  -h, --help
17
18
  `.trim()
18
19
 
19
20
 
20
- function main() {
21
+ export default function main() {
21
22
  const { values, positionals } = parseArgs({
22
23
  options: {
23
- 'left-delimiter': { type: 'string', default: '_' },
24
- 'right-delimiter': { type: 'string', default: '.' },
24
+ 'left-delimiter': { type: 'string', default: LEFT_DELIM },
25
+ 'right-delimiter': { type: 'string', default: RIGHT_DELIM },
25
26
  help: { short: 'h', type: 'boolean' },
26
27
  },
27
28
  allowPositionals: true,
@@ -32,18 +33,19 @@ function main() {
32
33
  return
33
34
  }
34
35
 
35
- const seq = extractSeqNums(
36
- readdirSync(positionals[0] || process.cwd()),
37
- values['left-delimiter'],
38
- values['right-delimiter'])
39
-
40
- const missing = findMissingNumbers(seq)
36
+ const dir = positionals[0] || process.cwd()
37
+ const missing = seqcheck(dir, values['left-delimiter'], values['right-delimiter'])
41
38
  if (missing.length)
42
39
  console.log('Missing:', missing)
43
40
  }
44
41
 
45
- export function extractSeqNums(names, leftDelimiter, rightDelimiter) {
46
- const pattern = new RegExp(escapeRegex(leftDelimiter) + '(\\d+)' + escapeRegex(rightDelimiter))
42
+ export function seqcheck(dir, leftDelim = LEFT_DELIM, rightDelim = RIGHT_DELIM) {
43
+ const seq = extractSeqNums(readdirSync(dir), leftDelim, rightDelim)
44
+ return findMissingNumbers(seq)
45
+ }
46
+
47
+ export function extractSeqNums(names, leftDelim, rightDelim) {
48
+ const pattern = new RegExp(escapeRegex(leftDelim) + '(\\d+)' + escapeRegex(rightDelim))
47
49
  const seq = []
48
50
  for (const name of names) {
49
51
  const match = name.match(pattern)
@@ -66,7 +68,3 @@ export function findMissingNumbers(seq) {
66
68
  function escapeRegex(str) {
67
69
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
68
70
  }
69
-
70
-
71
- if (import.meta.main)
72
- main()
package/src/sqcrop.js CHANGED
@@ -1,5 +1,3 @@
1
- #!/usr/bin/env node
2
-
3
1
  import { join } from 'node:path'
4
2
  import { rename } from 'node:fs/promises'
5
3
 
@@ -10,18 +8,18 @@ import { parseOptions } from './utils/parseOptions.js'
10
8
 
11
9
  const HELP = `
12
10
  SYNOPSIS
13
- mediasnacks sqcrop [-y | --overwrite] [--output-dir=<dir>] <images>
11
+ mediasnacks sqcrop [-y | --overwrite] [--outdir=<dir>] <images>
14
12
 
15
13
  DESCRIPTION
16
14
  Square crops images
17
15
  `.trim()
18
16
 
19
17
 
20
- async function main() {
18
+ export default async function main() {
21
19
  await assertUserHasFFmpeg()
22
20
 
23
21
  const { values, files } = await parseOptions({
24
- 'output-dir': { type: 'string', default: '' },
22
+ outdir: { type: 'string', default: '' },
25
23
  overwrite: { short: 'y', type: 'boolean' },
26
24
  help: { short: 'h', type: 'boolean' },
27
25
  })
@@ -36,26 +34,25 @@ async function main() {
36
34
 
37
35
  console.log('Cropping…')
38
36
  for (const file of files)
39
- await sqcrop({
40
- file,
41
- outFile: join(values['output-dir'], file),
42
- overwrite: values.overwrite
43
- })
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
+ }
44
48
  }
45
49
 
46
- async function sqcrop({ file, outFile, overwrite }) {
50
+ export async function sqcrop({ file, outFile, overwrite }) {
47
51
  const stOut = lstat(outFile)
48
52
 
49
- if (!overwrite && stOut?.isFile()) {
50
- console.log('(skipped: output file exists but --overwrite=false)', file)
51
- return
52
- }
53
- if (stOut?.mtimeMs > lstat(file)?.mtimeMs) {
54
- console.log('(skipped: outputFile is newer)', file)
55
- return
56
- }
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}`)
57
55
 
58
- console.log(file)
59
56
  const tmp = uniqueFilenameFor(file)
60
57
  await ffmpeg([
61
58
  '-v', 'error',
@@ -66,8 +63,3 @@ async function sqcrop({ file, outFile, overwrite }) {
66
63
  ])
67
64
  await rename(tmp, outFile)
68
65
  }
69
-
70
- main().catch(err => {
71
- console.error(err.message)
72
- process.exit(1)
73
- })
package/src/ssim.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { ffmpeg } from './utils/subprocess.js'
3
2
  import { parseOptions } from './utils/parseOptions.js'
4
3
 
@@ -12,7 +11,7 @@ DESCRIPTION
12
11
  `.trim()
13
12
 
14
13
 
15
- async function main() {
14
+ export default async function main() {
16
15
  const { values, positionals } = await parseOptions({
17
16
  help: { short: 'h', type: 'boolean' }
18
17
  })
@@ -41,10 +40,3 @@ export async function ssim(img1, img2) {
41
40
  throw new Error(`Could not parse SSIM output:\n${stderr}`)
42
41
  return parseFloat(match[1])
43
42
  }
44
-
45
-
46
- if (import.meta.main)
47
- main().catch(err => {
48
- console.error(err.message)
49
- process.exit(1)
50
- })
@@ -6,6 +6,7 @@ const glob = promisify(_glob)
6
6
 
7
7
  export async function parseOptions(options = {}, config = {}) {
8
8
  const { values, positionals, tokens } = parseArgs({
9
+ args: process.argv.slice(3),
9
10
  allowPositionals: true,
10
11
  options,
11
12
  ...config,
package/src/vsplit.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { readFileSync } from 'node:fs'
3
2
  import { resolve, parse, join } from 'node:path'
4
3
 
@@ -35,7 +34,7 @@ SEE ALSO
35
34
  `.trim()
36
35
 
37
36
 
38
- async function main() {
37
+ export default async function main() {
39
38
  await assertUserHasFFmpeg()
40
39
 
41
40
  const { values, files } = await parseOptions({
@@ -79,7 +78,7 @@ function parseCSV(csvPath) {
79
78
  return clips
80
79
  }
81
80
 
82
- async function vsplit(videoPath, clips) {
81
+ export async function vsplit(videoPath, clips) {
83
82
  const { dir, name, ext } = parse(videoPath)
84
83
  const seqLen = Math.log10(clips.length) + 1 | 0
85
84
 
@@ -97,8 +96,3 @@ async function vsplit(videoPath, clips) {
97
96
  ])
98
97
  }
99
98
  }
100
-
101
- main().catch(err => {
102
- console.error(err.message || err)
103
- process.exit(1)
104
- })
package/src/vtrim.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  import { resolve, parse } from 'node:path'
3
2
  import { parseOptions } from './utils/parseOptions.js'
4
3
  import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
@@ -14,14 +13,13 @@ DESCRIPTION
14
13
  OPTIONS
15
14
  -s, --start <time> Start time (e.g. 10, 00:00:10, 1:23.5). Default: beginning.
16
15
  -e, --end <time> End time (e.g. 30, 00:00:30, 2:45.0). Default: end of video.
17
- -h, --help
18
16
 
19
17
  SEE ALSO
20
18
  mediasnacks vsplit
21
19
  `.trim()
22
20
 
23
21
 
24
- async function main() {
22
+ export default async function main() {
25
23
  await assertUserHasFFmpeg()
26
24
 
27
25
  const { values, files } = await parseOptions({
@@ -39,10 +37,14 @@ async function main() {
39
37
  throw new Error('No video specified. See mediasnacks vtrim --help')
40
38
 
41
39
  for (const file of files)
42
- await vtrim(resolve(file), values.start, values.end)
40
+ await vtrim({
41
+ video: resolve(file),
42
+ start: values.start,
43
+ end: values.end
44
+ })
43
45
  }
44
46
 
45
- async function vtrim(video, start, end) {
47
+ export async function vtrim({ video, start, end }) {
46
48
  const { dir, name, ext } = parse(video)
47
49
  await ffmpeg([
48
50
  '-v', 'error',
@@ -54,8 +56,3 @@ async function vtrim(video, start, end) {
54
56
  resolve(dir, `${name}.trim${ext}`)
55
57
  ].flat())
56
58
  }
57
-
58
- main().catch(err => {
59
- console.error(err.message || err)
60
- process.exit(1)
61
- })