mediasnacks 0.22.5 → 0.24.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
@@ -26,6 +26,7 @@ mediasnacks <command> <args>
26
26
 
27
27
  ### Commands
28
28
  - `avif` Converts images to AVIF
29
+ - `png` Optimizes PNG images with oxipng
29
30
  - `sqcrop` Square crops images
30
31
 
31
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.22.5",
3
+ "version": "0.24.0",
4
4
  "description": "Utilities for optimizing and preparing videos and images",
5
5
  "license": "MIT",
6
6
  "author": "Eric Fortis",
package/src/avif.js CHANGED
@@ -29,7 +29,7 @@ async function main() {
29
29
 
30
30
  if (values.help) {
31
31
  console.log(HELP)
32
- process.exit(0)
32
+ return
33
33
  }
34
34
 
35
35
  if (!files.length)
package/src/cli.js CHANGED
@@ -8,6 +8,7 @@ import pkgJSON from '../package.json' with { type: 'json' }
8
8
 
9
9
  const COMMANDS = {
10
10
  avif: ['avif.js', 'Converts images to AVIF'],
11
+ png: ['png.sh', 'Optimizes PNG images with oxipng'],
11
12
  sqcrop: ['sqcrop.js', 'Square crops images\n'],
12
13
 
13
14
  resize: ['resize.js', 'Resizes videos or images'],
@@ -59,11 +60,11 @@ function main() {
59
60
 
60
61
  if (opt === '-v' || opt === '--version') {
61
62
  console.log(pkgJSON.version)
62
- process.exit(0)
63
+ return
63
64
  }
64
65
  if (opt === '-h' || opt === '--help') {
65
66
  console.log(HELP)
66
- process.exit(0)
67
+ return
67
68
  }
68
69
 
69
70
  if (!opt) {
package/src/detectdups.js CHANGED
@@ -41,7 +41,7 @@ async function main() {
41
41
 
42
42
  if (values.help) {
43
43
  console.log(HELP)
44
- process.exit(0)
44
+ return
45
45
  }
46
46
 
47
47
  if (files.length !== 1)
@@ -62,15 +62,9 @@ async function main() {
62
62
  ? Number(values.duration)
63
63
  : vDur > 60 ? 20 : vDur
64
64
 
65
- if (isNaN(seek) || seek < 0)
66
- throw new Error(`Invalid --seek value: ${values.seek}`)
67
-
68
- if (isNaN(duration) || duration < 1)
69
- throw new Error(`Invalid --duration value: ${values.duration}`)
70
-
71
- if ((seek + duration) > vDur)
72
- throw new Error(`Invalid analysis range. Exceeds video duration: ${vDur}`)
73
-
65
+ if (isNaN(seek) || seek < 0) throw new Error(`Invalid --seek value: ${values.seek}`)
66
+ if (isNaN(duration) || duration < 1) throw new Error(`Invalid --duration value: ${values.duration}`)
67
+ if ((seek + duration) > vDur) throw new Error(`Invalid analysis range. Exceeds video duration: ${vDur}`)
74
68
 
75
69
  const dups = await detectdups(files[0], seek, duration)
76
70
  const h = deltaHistogram(dups)
package/src/dropdups.js CHANGED
@@ -4,10 +4,10 @@ import { resolve, parse, format } from 'node:path'
4
4
 
5
5
  import { parseOptions } from './utils/parseOptions.js'
6
6
  import { ffmpeg, assertUserHasFFmpeg, run } from './utils/subprocess.js'
7
- import { PRORES_PROFILES } from './prores.js'
7
+ import { ProresProfiles } from './prores.js'
8
8
 
9
9
 
10
- const PROFILE = PRORES_PROFILES.hq
10
+ const PROFILE = ProresProfiles.default
11
11
 
12
12
  const HELP = `
13
13
  SYNOPSIS
@@ -40,7 +40,7 @@ async function main() {
40
40
 
41
41
  if (values.help) {
42
42
  console.log(HELP)
43
- process.exit(0)
43
+ return
44
44
  }
45
45
 
46
46
  if (!files.length)
@@ -65,9 +65,7 @@ async function dropdups(video, dupFrameNum) {
65
65
  ? `decimate=cycle=${dupFrameNum}`
66
66
  : 'mpdecimate,setpts=N/FRAME_RATE/TB',
67
67
  '-fps_mode', 'cfr',
68
- '-c:v', 'prores_ks',
69
- '-profile:v', PROFILE,
70
- '-pix_fmt', 'yuv422p10le',
68
+ '-c:v', 'prores_ks', '-profile:v', PROFILE,
71
69
  makeOutputPath(video)
72
70
  ])
73
71
  }
package/src/edgespic.js CHANGED
@@ -33,7 +33,7 @@ async function main() {
33
33
 
34
34
  if (values.help) {
35
35
  console.log(HELP)
36
- process.exit(0)
36
+ return
37
37
  }
38
38
 
39
39
  const width = Number(values['width'])
package/src/flattendir.sh CHANGED
@@ -9,17 +9,11 @@ DESCRIPTION
9
9
  Moves unique files from subdirectories into the top-level folder, then
10
10
  deletes empty directories. Defaults to the current working directory.
11
11
  EOF
12
+ exit "${1:-0}"
12
13
  }
13
14
 
14
- if [ "$1" = "-h" ]; then
15
- help
16
- exit 0
17
- fi
18
-
19
- if [ $# -gt 0 ] && [ ! -d "$1" ]; then
20
- help
21
- exit 1
22
- fi
15
+ [ "$1" = "-h" ] && help
16
+ [ $# -gt 0 ] && [ ! -d "$1" ] && help 1
23
17
 
24
18
  DIR="${1:-$(pwd)}"
25
19
 
package/src/framediff.sh CHANGED
@@ -16,17 +16,11 @@ TIPS
16
16
  SEE ALSO
17
17
  mediasnacks detectdups, ffplay(1)
18
18
  EOF
19
+ exit "${1:-0}"
19
20
  }
20
21
 
21
- if [ "$1" = "-h" ]; then
22
- help
23
- exit 0
24
- fi
25
-
26
- if [ ! -f "$1" ]; then
27
- help
28
- exit 1
29
- fi
22
+ [ "$1" = "-h" ] && help
23
+ [ ! -f "$1" ] && help 1
30
24
 
31
25
  ffplay -v error "$1" -vf "
32
26
  tblend=all_mode=difference,
package/src/gif.sh CHANGED
@@ -1,13 +1,21 @@
1
1
  #!/bin/sh
2
2
 
3
- # Converts to GIF
4
-
5
3
  FPS=10
6
4
  WIDTH=600
7
5
 
8
- usage() {
9
- echo "Usage: mediasnacks gif [--fps <number>] [--width <pixels>] <file>"
10
- exit 1
6
+ help() {
7
+ /bin/cat << EOF
8
+ SYNOPSIS
9
+ mediasnacks gif [--fps <number>] [-w | --width <pixels>] <file>
10
+
11
+ DESCRIPTION
12
+ Converts video to GIF
13
+
14
+ OPTIONS
15
+ --fps Default: $FPS
16
+ -w,--width Default: $WIDTH
17
+ EOF
18
+ exit "${1:-0}"
11
19
  }
12
20
 
13
21
  while [ $# -gt 0 ]; do
@@ -15,18 +23,21 @@ while [ $# -gt 0 ]; do
15
23
  --fps)
16
24
  FPS="$2";
17
25
  shift 2 ;;
18
- --width)
26
+
27
+ --width|-w)
19
28
  WIDTH="$2"
20
29
  shift 2 ;;
30
+
21
31
  --help|-h)
22
- usage ;;
32
+ help ;;
33
+
23
34
  *)
24
35
  file="$1"
25
36
  shift ;;
26
37
  esac
27
38
  done
28
39
 
29
- [ -z "$file" ] && usage
40
+ [ -z "$file" ] && help 1
30
41
 
31
42
  ffmpeg -v error -i "$file" \
32
43
  -vf "fps=${FPS},scale=${WIDTH}:-1" \
package/src/hev1tohvc1.js CHANGED
@@ -19,12 +19,18 @@ DESCRIPTION
19
19
  async function main() {
20
20
  await assertUserHasFFmpeg()
21
21
 
22
- const { files } = await parseOptions()
22
+ const { values, files } = await parseOptions({
23
+ help: { short: 'h', type: 'boolean' }
24
+ })
25
+
26
+ if (values.help) {
27
+ console.log(HELP)
28
+ return
29
+ }
23
30
 
24
31
  if (!files.length)
25
32
  throw new Error(HELP)
26
33
 
27
- console.log('HEV1 to HVC1…')
28
34
  for (const file of files)
29
35
  await hev1tohvc1(file)
30
36
  }
package/src/moov2front.js CHANGED
@@ -23,7 +23,14 @@ NOTES
23
23
  async function main() {
24
24
  await assertUserHasFFmpeg()
25
25
 
26
- const { files } = await parseOptions()
26
+ const { values, files } = await parseOptions({
27
+ help: { short: 'h', type: 'boolean' }
28
+ })
29
+
30
+ if (values.help) {
31
+ console.log(HELP)
32
+ return
33
+ }
27
34
 
28
35
  if (!files.length)
29
36
  throw new Error(HELP)
package/src/play.js CHANGED
@@ -25,7 +25,7 @@ async function main() {
25
25
 
26
26
  if (values.help) {
27
27
  console.log(HELP)
28
- process.exit(0)
28
+ return
29
29
  }
30
30
 
31
31
  const files = findFiles({
@@ -54,6 +54,7 @@ function play(files) {
54
54
  console.error('Error: MPV is not installed')
55
55
  else
56
56
  console.log(err)
57
+ process.exit(1)
57
58
  })
58
59
  }
59
60
 
package/src/png.sh ADDED
@@ -0,0 +1,20 @@
1
+ #!/bin/sh
2
+
3
+ help() {
4
+ /bin/cat << EOF
5
+ SYNOPSIS
6
+ mediasnacks png <img1> [img2 ...]
7
+
8
+ DESCRIPTION
9
+ Losslessly optimizes PNG images with oxipng at max level.
10
+
11
+ EXAMPLE
12
+ mediasnacks png *.png
13
+ EOF
14
+ exit "${1:-0}"
15
+ }
16
+
17
+ [ "$1" = "-h" ] && help
18
+ [ $# -eq 0 ] && help 1
19
+
20
+ oxipng --opt max "$@"
package/src/prores.js CHANGED
@@ -3,13 +3,24 @@ import { resolve, parse, join } from 'node:path'
3
3
  import { parseOptions } from './utils/parseOptions.js'
4
4
  import { assertUserHasFFmpeg, run } from './utils/subprocess.js'
5
5
 
6
- export const PRORES_PROFILES = {
7
- 'proxy': 0,
8
- 'lt': 1,
9
- 'standard': 2,
10
- 'hq': 3,
11
- '4444': 4,
12
- '4444xq': 5,
6
+ export const ProresProfiles = new class {
7
+ // https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
8
+ profiles = {
9
+ // 10-bit
10
+ 0: 'proxy',
11
+ 1: 'lt',
12
+ 2: 'standard',
13
+ 3: 'hq',
14
+
15
+ // 12-bit
16
+ 4: '4444',
17
+ 5: '4444xq'
18
+ }
19
+ default = 3
20
+
21
+ list = () => Object.keys(this.profiles)
22
+ isValid = n => Object.hasOwn(this.profiles, n)
23
+ table = () => Object.entries(this.profiles)
13
24
  }
14
25
 
15
26
  const HELP = `
@@ -17,15 +28,21 @@ SYNOPSIS
17
28
  mediasnacks prores [options] <video>
18
29
 
19
30
  DESCRIPTION
20
- Converts a video to ProRes format.
31
+ Converts a video to ProRes
21
32
 
22
33
  OPTIONS
23
- -p, --profile <n> ProRes profile (default: 3 (422 HQ))
34
+ -p, --profile <n> Default: 3 (422 HQ 10-bit)
35
+ -s, --start <time> Start time (e.g. 5.0). Default: beginning.
36
+ -e, --end <time> End time (e.g. 0:10.0). Default: end of video.
24
37
  -h, --help
25
38
 
39
+ PROFILES
40
+ ${ProresProfiles.table().map(([num, name]) =>
41
+ ` ${num}: ${name}`).join('\n')}
42
+
26
43
  EXAMPLES
27
44
  mediasnacks prores video.mov
28
- mediasnacks prores --profile 2 video.mov
45
+ mediasnacks prores -p2 *.mov
29
46
 
30
47
  Both output a file named: video.prores.mov
31
48
  `.trim()
@@ -35,15 +52,20 @@ async function main() {
35
52
  await assertUserHasFFmpeg()
36
53
 
37
54
  const { values, files } = await parseOptions({
38
- profile: { short: 'p', type: 'string', default: String(PRORES_PROFILES.hq) },
55
+ profile: { short: 'p', type: 'string', default: String(ProresProfiles.default) },
56
+ start: { short: 's', type: 'string', default: '' },
57
+ end: { short: 'e', type: 'string', default: '' },
39
58
  help: { short: 'h', type: 'boolean' },
40
59
  })
41
60
 
42
61
  if (values.help) {
43
62
  console.log(HELP)
44
- process.exit(0)
63
+ return
45
64
  }
46
65
 
66
+ if (!ProresProfiles.isValid(Number(values.profile)))
67
+ throw new Error('Invalid profile. Must be one of: ' + ProresProfiles.list().join(','))
68
+
47
69
  if (files.length !== 1)
48
70
  throw new Error('Expected 1 argument: video file. See mediasnacks prores --help')
49
71
 
@@ -51,20 +73,19 @@ async function main() {
51
73
  const { name, dir } = parse(video)
52
74
  const output = join(dir, `${name}.prores.mov`)
53
75
 
54
- console.log(`Converting to ProRes…`)
55
- await prores(video, values.profile, output)
76
+ await prores(video, values.start, values.end, values.profile, output)
56
77
  }
57
78
 
58
- async function prores(video, profile, output) {
79
+ async function prores(video, start, end, profile, output) {
59
80
  await run('ffmpeg', [
60
81
  '-v', 'error',
61
82
  '-stats',
83
+ start ? ['-ss', start] : [],
84
+ end ? ['-to', end] : [],
62
85
  '-i', video,
63
- '-c:v', 'prores_ks',
64
- '-profile:v', profile,
65
- '-pix_fmt', 'yuv422p10le',
86
+ '-c:v', 'prores_ks', '-profile:v', profile,
66
87
  output
67
- ])
88
+ ].flat())
68
89
  }
69
90
 
70
91
  if (import.meta.main)
package/src/qdir.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { join } from 'node:path'
4
4
  import { spawn } from 'node:child_process'
5
- import { parseArgs } from 'node:util'
6
5
  import { readdir, writeFile, unlink, rename } from 'node:fs/promises'
7
6
 
8
7
  import { isFile } from './utils/fs-utils.js'
8
+ import { parseOptions } from './utils/parseOptions.js'
9
9
 
10
10
 
11
11
  const HELP = `
@@ -14,20 +14,30 @@ SYNOPSIS
14
14
 
15
15
  DESCRIPTION
16
16
  Sequentially runs all *.sh files in a folder (cwd by default).
17
+ Completed scripts get renamed with a ".done" extension,
18
+ or to ".failed.$exitCode"
17
19
  `.trim()
18
20
 
19
21
 
22
+ function filter(f) {
23
+ return f.endsWith('.sh')
24
+ }
25
+
26
+ function newExt(exitCode) {
27
+ return exitCode === 0
28
+ ? '.done'
29
+ : `.failed.${exitCode}`
30
+ }
31
+
32
+
20
33
  async function main() {
21
- const { values, positionals } = parseArgs({
22
- options: {
23
- help: { short: 'h', type: 'boolean' },
24
- },
25
- allowPositionals: true,
34
+ const { values, positionals } = await parseOptions({
35
+ help: { short: 'h', type: 'boolean' }
26
36
  })
27
37
 
28
38
  if (values.help) {
29
39
  console.log(HELP)
30
- process.exit(0)
40
+ return
31
41
  }
32
42
 
33
43
  const dir = positionals[0] || process.cwd()
@@ -55,12 +65,9 @@ export async function qdir(dir, pollIntervalMs = 10_000) {
55
65
 
56
66
  const jobName = job.split('/').pop()
57
67
  await writeFile(lock, jobName, 'utf8')
58
-
59
68
  try {
60
69
  const exitCode = await runShell(job)
61
- await rename(job, job + (exitCode === 0
62
- ? '.done'
63
- : `.failed.${exitCode}`))
70
+ await rename(job, job + newExt(exitCode))
64
71
  }
65
72
  finally {
66
73
  await unlink(lock).catch(() => {})
@@ -71,8 +78,8 @@ export async function qdir(dir, pollIntervalMs = 10_000) {
71
78
  async function getNextJob(dir) {
72
79
  const entries = await readdir(dir, { withFileTypes: true })
73
80
  const scripts = entries
74
- .filter(d => d.isFile() && d.name.endsWith('.sh'))
75
- .map(d => d.name)
81
+ .filter(entry => entry.isFile() && filter(entry.name))
82
+ .map(entry => entry.name)
76
83
  .sort()
77
84
  return scripts.length
78
85
  ? join(dir, scripts[0])
package/src/random.js CHANGED
@@ -14,10 +14,8 @@ DESCRIPTION
14
14
  `.trim()
15
15
 
16
16
  async function main() {
17
- if (process.platform !== 'darwin') {
18
- console.error('Error: This command is only supported on macOS.')
19
- process.exit(1)
20
- }
17
+ if (process.platform !== 'darwin')
18
+ throw new Error('Error: This command is only supported on macOS.')
21
19
 
22
20
  const { values } = await parseOptions({
23
21
  recursive: { short: 'r', type: 'boolean' },
@@ -26,7 +24,7 @@ async function main() {
26
24
 
27
25
  if (values.help) {
28
26
  console.log(HELP)
29
- process.exit(0)
27
+ return
30
28
  }
31
29
 
32
30
  spawn('open', [pickRandomFile('.', values.recursive)])
package/src/resize.js CHANGED
@@ -41,7 +41,7 @@ async function main() {
41
41
 
42
42
  if (values.help) {
43
43
  console.log(HELP)
44
- process.exit(0)
44
+ return
45
45
  }
46
46
 
47
47
  const width = Number(values.width)
package/src/seqcheck.js CHANGED
@@ -29,7 +29,7 @@ function main() {
29
29
 
30
30
  if (values.help) {
31
31
  console.log(HELP)
32
- process.exit(0)
32
+ return
33
33
  }
34
34
 
35
35
  const seq = extractSeqNums(
package/src/sqcrop.js CHANGED
@@ -28,7 +28,7 @@ async function main() {
28
28
 
29
29
  if (values.help) {
30
30
  console.log(HELP)
31
- process.exit(0)
31
+ return
32
32
  }
33
33
 
34
34
  if (!files.length)
package/src/ssim.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { ffmpeg } from './utils/subprocess.js'
3
+ import { parseOptions } from './utils/parseOptions.js'
3
4
 
4
5
 
5
6
  const HELP = `
@@ -7,19 +8,25 @@ SYNOPSIS
7
8
  mediasnacks ssim <img1> <img2>
8
9
 
9
10
  DESCRIPTION
10
- Computes the Structural Similarity Index (SSIM) between two images using ffmpeg.
11
+ Computes the Structural Similarity Index (SSIM) between two images using FFmpeg.
11
12
  `.trim()
12
13
 
13
14
 
14
15
  async function main() {
15
- const [img1, img2] = process.argv.slice(2)
16
- if (!img1 || !img2) {
16
+ const { values, positionals } = await parseOptions({
17
+ help: { short: 'h', type: 'boolean' }
18
+ })
19
+
20
+ if (values.help) {
17
21
  console.log(HELP)
18
- process.exit(1)
22
+ return
19
23
  }
20
24
 
21
- const score = await ssim(img1, img2)
22
- console.log(score)
25
+ if (positionals.length !== 2)
26
+ throw new Error('Expected two images')
27
+
28
+ const score = await ssim(...positionals)
29
+ console.log(score.toString())
23
30
  }
24
31
 
25
32
  export async function ssim(img1, img2) {
@@ -39,10 +39,9 @@ async function runSilently(program, args) {
39
39
 
40
40
  export async function run(program, args) {
41
41
  return new Promise((resolve, reject) => {
42
- const p = spawn(program, args)
43
- p.stdout.on('data', data => process.stdout.write(data))
44
- p.stderr.on('data', chunk => process.stderr.write(chunk))
45
-
42
+ const p = spawn(program, args, { stdio: ['inherit', 'pipe', 'pipe'] })
43
+ p.stdout.pipe(process.stdout)
44
+ p.stderr.pipe(process.stderr)
46
45
  p.on('error', reject)
47
46
  p.on('close', code => {
48
47
  if (code === 0)
package/src/vconcat.sh CHANGED
@@ -7,30 +7,21 @@ SYNOPSIS
7
7
  mediasnacks vconcat <video1> <video2> …
8
8
 
9
9
  DESCRIPTION
10
- Concatenates video files using FFmpeg's without re-encoding.
10
+ Concatenates video files using FFmpeg without re-encoding.
11
11
  All videos must have compatible codecs and resolutions.
12
12
 
13
13
  EXAMPLES
14
14
  mediasnacks vconcat vid1.mov vid2.mov
15
15
  mediasnacks vconcat *.mp4
16
16
  EOF
17
+ exit "${1:-0}"
17
18
  }
18
19
 
19
- if [ "$1" = "-h" ]; then
20
- help
21
- exit 0
22
- fi
23
-
24
- if [ "$#" -lt 2 ]; then
25
- help
26
- exit 1
27
- fi
20
+ [ "$1" = "-h" ] && help
21
+ [ "$#" -lt 2 ] && help 1
28
22
 
29
23
  for arg in "$@"; do
30
- if [ ! -f "$arg" ]; then
31
- help
32
- exit 1
33
- fi
24
+ [ ! -f "$arg" ] && help 1
34
25
  done
35
26
 
36
27
  list_file=$(mktemp -p .)
package/src/vdiff.sh CHANGED
@@ -10,17 +10,11 @@ DESCRIPTION
10
10
  Diffs two video files using FFplay with a blend filter.
11
11
  Videos must have the same resolution.
12
12
  EOF
13
+ exit "${1:-0}"
13
14
  }
14
15
 
15
- if [ "$1" = "-h" ]; then
16
- help
17
- exit 0
18
- fi
19
-
20
- if [ $# -lt 2 ] || [ ! -f "$1" ] || [ ! -f "$2" ]; then
21
- help
22
- exit 1
23
- fi
16
+ [ "$1" = "-h" ] && help
17
+ [ $# -lt 2 ] || [ ! -f "$1" ] || [ ! -f "$2" ] && help 1
24
18
 
25
19
  video1="$1"
26
20
  video2="$2"
package/src/vsplit.js CHANGED
@@ -44,7 +44,7 @@ async function main() {
44
44
 
45
45
  if (values.help) {
46
46
  console.log(HELP)
47
- process.exit(0)
47
+ return
48
48
  }
49
49
 
50
50
  if (files.length !== 2)
package/src/vtrim.js CHANGED
@@ -32,7 +32,7 @@ async function main() {
32
32
 
33
33
  if (values.help) {
34
34
  console.log(HELP)
35
- process.exit(0)
35
+ return
36
36
  }
37
37
 
38
38
  if (!files.length)