mediasnacks 0.12.0 → 0.12.2

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
@@ -38,26 +38,23 @@ Commands:
38
38
 
39
39
  ### Glob Patterns and Literal Filenames
40
40
 
41
- Most commands accept glob patterns (like `*.png` or `file[234].png`) to match multiple files. By default, these patterns are expanded by Node.js to match existing files.
41
+ Most commands accept glob patterns (like `*.png` or `file[234].png`) to match multiple
42
+ files. By default, these patterns are expanded by Node.js to match existing files.
42
43
 
43
- To treat arguments as literal filenames instead of glob patterns, use the `--` (double dash) separator:
44
+ To treat arguments as literal filenames instead of
45
+ glob patterns, use the `--` (double dash) separator:
44
46
 
45
47
  ```shell
46
- # This expands the pattern to match files: file2.png, file3.png, file4.png
48
+ # Expands to: file2.png, file3.png, file4.png
47
49
  npx mediasnacks avif file[234].png
48
50
 
49
- # This treats the argument as a literal filename: "file[234].png"
51
+ # Literal filename: "file[234].png"
50
52
  npx mediasnacks avif -- file[234].png
51
53
 
52
- # You can mix both: expand first pattern, treat second as literal
54
+ # Mixed: expand first pattern, treat second as literal
53
55
  npx mediasnacks avif file2.png -- file[234].png
54
56
  ```
55
57
 
56
- This follows the standard POSIX convention where `--` stops option/pattern processing. Arguments after `--` are treated exactly as written, which is useful when:
57
- - Filenames contain special glob characters like `[`, `]`, `*`, or `?`
58
- - You want to pass a pattern to be processed later
59
- - Working with filenames that haven't been created yet
60
-
61
58
  <br/>
62
59
 
63
60
  ### Converting Images to AVIF
@@ -98,7 +95,7 @@ Rearranges .mov and .mp4 metadata to the start of the file for fast-start stream
98
95
  ```shell
99
96
  npx mediasnacks moov2front <videos>
100
97
  ```
101
- hat is Fast Start?
98
+ What is Fast Start?
102
99
  - https://wiki.avblocks.com/avblocks-for-cpp/muxer-parameters/mp4
103
100
  - https://trac.ffmpeg.org/wiki/HowToCheckIfFaststartIsEnabledForPlayback
104
101
 
package/TODO.md CHANGED
@@ -2,3 +2,6 @@
2
2
 
3
3
  - Test on Windows
4
4
  - Test on Linux
5
+
6
+
7
+ Handle when shell expands, but files have special chars
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Utilities for preparing videos, images, and audio for the web",
5
5
  "license": "MIT",
6
6
  "author": "Eric Fortis",
package/src/avif.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { join } from 'node:path'
3
+ import { join, basename } from 'node:path'
4
4
 
5
+ import { parseOptions } from './utils/parseOptions.js'
5
6
  import { replaceExt, lstat } from './utils/fs-utils.js'
6
- import { parseArgsWithGlobs } from './utils/args-with-globs.js'
7
7
  import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
8
8
 
9
9
 
@@ -17,12 +17,10 @@ Converts images to AVIF.
17
17
  async function main() {
18
18
  await assertUserHasFFmpeg()
19
19
 
20
- const { values, files } = await parseArgsWithGlobs({
21
- options: {
22
- 'output-dir': { type: 'string', default: '' },
23
- overwrite: { short: 'y', type: 'boolean', default: false },
24
- help: { short: 'h', type: 'boolean', default: false },
25
- }
20
+ const { values, files } = await parseOptions({
21
+ 'output-dir': { type: 'string', default: '' },
22
+ overwrite: { short: 'y', type: 'boolean', default: false },
23
+ help: { short: 'h', type: 'boolean', default: false },
26
24
  })
27
25
 
28
26
  if (values.help) {
@@ -37,7 +35,7 @@ async function main() {
37
35
  for (const file of files)
38
36
  await toAvif({
39
37
  file,
40
- outFile: join(values['output-dir'], replaceExt(file, 'avif')),
38
+ outFile: join(values['output-dir'], replaceExt(basename(file), 'avif')),
41
39
  overwrite: values.overwrite
42
40
  })
43
41
  }
package/src/dropdups.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { resolve, parse, format } from 'node:path'
4
4
 
5
- import { parseArgsWithGlobs } from './utils/args-with-globs.js'
5
+ import { parseOptions } from './utils/parseOptions.js'
6
6
  import { ffmpeg, assertUserHasFFmpeg, run } from './utils/ffmpeg.js'
7
7
 
8
8
 
@@ -34,11 +34,9 @@ Options:
34
34
  async function main() {
35
35
  await assertUserHasFFmpeg()
36
36
 
37
- const { values, files } = await parseArgsWithGlobs({
38
- options: {
39
- 'bad-frame-number': { short: 'n', type: 'string', default: '' },
40
- help: { short: 'h', type: 'boolean', default: false },
41
- }
37
+ const { values, files } = await parseOptions({
38
+ 'bad-frame-number': { short: 'n', type: 'string', default: '' },
39
+ help: { short: 'h', type: 'boolean', default: false },
42
40
  })
43
41
 
44
42
  if (values.help) {
package/src/hev1tohvc1.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { parseOptions } from './utils/parseOptions.js'
3
4
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
4
- import { parseArgsWithGlobs } from './utils/args-with-globs.js'
5
5
  import { videoAttrs, ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
6
6
 
7
7
 
@@ -17,7 +17,7 @@ by changing the container’s sample entry code from HEV1 to HVC1.
17
17
  async function main() {
18
18
  await assertUserHasFFmpeg()
19
19
 
20
- const { files } = await parseArgsWithGlobs({})
20
+ const { files } = await parseOptions()
21
21
 
22
22
  if (!files.length)
23
23
  throw new Error(USAGE)
package/src/moov2front.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
4
4
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
5
- import { parseArgsWithGlobs } from './utils/args-with-globs.js'
5
+ import { parseOptions } from './utils/parseOptions.js'
6
6
 
7
7
 
8
8
  const USAGE = `
@@ -16,7 +16,7 @@ Files are overwritten.
16
16
  async function main() {
17
17
  await assertUserHasFFmpeg()
18
18
 
19
- const { files } = await parseArgsWithGlobs({})
19
+ const { files } = await parseOptions()
20
20
 
21
21
  if (!files.length)
22
22
  throw new Error(USAGE)
package/src/qdir.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import { join } from 'node:path'
3
4
  import { spawn } from 'node:child_process'
4
5
  import { parseArgs } from 'node:util'
5
- import { resolve, join } from 'node:path'
6
+ import { fileURLToPath } from 'node:url'
6
7
  import { readdir, writeFile, unlink, rename } from 'node:fs/promises'
8
+
7
9
  import { isFile } from './utils/fs-utils.js'
8
- import { fileURLToPath } from 'node:url'
9
10
 
10
11
 
11
12
  const USAGE = `
package/src/resize.js CHANGED
@@ -3,8 +3,8 @@
3
3
  import { join } from 'node:path'
4
4
  import { rename } from 'node:fs/promises'
5
5
 
6
+ import { parseOptions } from './utils/parseOptions.js'
6
7
  import { isFile, uniqueFilenameFor } from './utils/fs-utils.js'
7
- import { parseArgsWithGlobs } from './utils/args-with-globs.js'
8
8
  import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/ffmpeg.js'
9
9
 
10
10
 
@@ -30,14 +30,12 @@ Details:
30
30
  async function main() {
31
31
  await assertUserHasFFmpeg()
32
32
 
33
- const { values, files } = await parseArgsWithGlobs({
34
- options: {
35
- width: { type: 'string', default: '-2' },
36
- height: { type: 'string', default: '-2' },
37
- 'output-dir': { type: 'string', default: '' },
38
- overwrite: { short: 'y', type: 'boolean', default: false },
39
- help: { short: 'h', type: 'boolean', default: false },
40
- }
33
+ const { values, files } = await parseOptions({
34
+ width: { type: 'string', default: '-2' },
35
+ height: { type: 'string', default: '-2' },
36
+ 'output-dir': { type: 'string', default: '' },
37
+ overwrite: { short: 'y', type: 'boolean', default: false },
38
+ help: { short: 'h', type: 'boolean', default: false },
41
39
  })
42
40
 
43
41
  if (values.help) {
@@ -59,7 +57,7 @@ async function main() {
59
57
  for (const file of files)
60
58
  await resize({
61
59
  file,
62
- outFile: join(values['output-dir'], file),
60
+ outFile: join(values['output-dir'], file), // TODO basename ?
63
61
  overwrite: values.overwrite,
64
62
  width,
65
63
  height,
package/src/seqcheck.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { resolve } from 'node:path'
4
3
  import { parseArgs } from 'node:util'
5
4
  import { readdirSync } from 'node:fs'
6
5
  import { fileURLToPath } from 'node:url'
package/src/sqcrop.js CHANGED
@@ -4,8 +4,8 @@ import { join } from 'node:path'
4
4
 
5
5
  import { rename } from 'node:fs/promises'
6
6
  import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
7
- import { replaceExt, lstat, uniqueFilenameFor } from './utils/fs-utils.js'
8
- import { parseArgsWithGlobs } from './utils/args-with-globs.js'
7
+ import { lstat, uniqueFilenameFor } from './utils/fs-utils.js'
8
+ import { parseOptions } from './utils/parseOptions.js'
9
9
 
10
10
 
11
11
  const USAGE = `
@@ -18,12 +18,10 @@ Square crops images
18
18
  async function main() {
19
19
  await assertUserHasFFmpeg()
20
20
 
21
- const { values, files } = await parseArgsWithGlobs({
22
- options: {
23
- 'output-dir': { type: 'string', default: '' },
24
- overwrite: { short: 'y', type: 'boolean', default: false },
25
- help: { short: 'h', type: 'boolean', default: false },
26
- }
21
+ const { values, files } = await parseOptions({
22
+ 'output-dir': { type: 'string', default: '' },
23
+ overwrite: { short: 'y', type: 'boolean', default: false },
24
+ help: { short: 'h', type: 'boolean', default: false },
27
25
  })
28
26
 
29
27
  if (values.help) {
@@ -1,7 +1,7 @@
1
+ import { lstatSync } from 'node:fs'
1
2
  import { randomUUID } from 'node:crypto'
2
3
  import { unlink, rename } from 'node:fs/promises'
3
4
  import { dirname, extname, join } from 'node:path'
4
- import { lstatSync } from 'node:fs'
5
5
 
6
6
 
7
7
  export const lstat = f => lstatSync(f, { throwIfNoEntry: false })
@@ -0,0 +1,39 @@
1
+ import { promisify, parseArgs } from 'node:util'
2
+ import { glob as _glob } from 'node:fs'
3
+
4
+
5
+ const glob = promisify(_glob)
6
+
7
+ export async function parseOptions(options = {}, config = {}) {
8
+ const { values, positionals, tokens } = parseArgs({
9
+ allowPositionals: true,
10
+ options,
11
+ ...config,
12
+ tokens: true
13
+ })
14
+ return {
15
+ values,
16
+ files: await resolveGlobs(positionals, tokens)
17
+ }
18
+ }
19
+
20
+ async function resolveGlobs(arr, tokens = []) {
21
+ const terminatorIndex = tokens.find(t => t.kind === 'option-terminator')?.index ?? -1
22
+ const set = new Set()
23
+
24
+ const globable = terminatorIndex === -1
25
+ ? arr
26
+ : arr.slice(0, terminatorIndex)
27
+
28
+ for (const g of globable)
29
+ for (const file of await glob(g))
30
+ set.add(file)
31
+
32
+
33
+ if (terminatorIndex !== -1)
34
+ for (const literal of arr.slice(terminatorIndex))
35
+ set.add(literal)
36
+
37
+ return Array.from(set)
38
+ }
39
+
@@ -0,0 +1,59 @@
1
+ import { join } from 'node:path'
2
+ import { tmpdir } from 'node:os'
3
+ import { equal, deepEqual } from 'node:assert/strict'
4
+ import { mkdtemp, writeFile, rm } from 'node:fs/promises'
5
+ import { test, describe, before, after } from 'node:test'
6
+
7
+ import { parseOptions } from './parseOptions.js'
8
+
9
+
10
+ describe('parseArgsWithGlobs', () => {
11
+ let testDir
12
+ let inTmpDir = f => join(testDir, f)
13
+ const testFiles = ['file1.png', 'file2.png', 'file3.png']
14
+
15
+ before(async () => {
16
+ testDir = await mkdtemp(join(tmpdir(), 'parse-args-'))
17
+ for (const file of testFiles)
18
+ await writeFile(inTmpDir(file), '')
19
+ })
20
+
21
+ after(() => rm(testDir))
22
+
23
+ test('parses args and globs files', async () => {
24
+ const { values, files } = await parseOptions({
25
+ 'output-dir': { type: 'string' }
26
+ }, {
27
+ args: ['--output-dir', '/tmp', inTmpDir('file[12].png')],
28
+ })
29
+ equal(values['output-dir'], '/tmp')
30
+ deepEqual(files, [
31
+ inTmpDir('file1.png'),
32
+ inTmpDir('file2.png')
33
+ ])
34
+ })
35
+
36
+ test('respects verbatim tokens', async () => {
37
+ const literal0 = 'literal-file[98].png'
38
+ const literal1 = 'literal-file[99].png'
39
+ const { files } = await parseOptions({}, {
40
+ args: [inTmpDir('file[12].png'), '--', literal0, literal1]
41
+ })
42
+ deepEqual(files, [
43
+ inTmpDir('file1.png'),
44
+ inTmpDir('file2.png'),
45
+ literal0,
46
+ literal1,
47
+ ])
48
+ })
49
+
50
+ test('empty files array when no positionals', async () => {
51
+ const { files, values } = await parseOptions({
52
+ foo: { type: 'boolean' }
53
+ }, {
54
+ args: ['--foo'],
55
+ })
56
+ equal(values.foo, true)
57
+ deepEqual(files, [])
58
+ })
59
+ })
package/src/vconcat.sh CHANGED
@@ -11,7 +11,7 @@ fi
11
11
 
12
12
  list_file=$(mktemp -p .)
13
13
  for file in "$@"; do
14
- echo "file '$file'" >> "$list_file"
14
+ printf "file %q\n" "$file" >> "$list_file"
15
15
  done
16
16
 
17
17
  first_video="$1"
@@ -0,0 +1,22 @@
1
+ import { join } from 'node:path'
2
+ import { test } from 'node:test'
3
+ import { equal } from 'node:assert/strict'
4
+ import { tmpdir } from 'node:os'
5
+ import { execSync } from 'node:child_process'
6
+ import { createHash } from 'node:crypto'
7
+ import { mkdtempSync, readFileSync } from 'node:fs'
8
+
9
+ function sha1(filePath) {
10
+ return createHash('sha1').update(readFileSync(filePath)).digest('hex')
11
+ }
12
+
13
+ test('PNG to AVIF', () => {
14
+ const tmp = mkdtempSync(join(tmpdir(), 'avif-test-'))
15
+
16
+ execSync(`src/cli.js avif --output-dir ${tmp} tests/fixtures/lenna.png`, {
17
+ cwd: process.cwd(),
18
+ stdio: 'inherit'
19
+ })
20
+
21
+ equal(sha1(join(tmp, 'lenna.avif')), sha1('tests/fixtures/lenna.avif'))
22
+ })
Binary file
Binary file
@@ -1,61 +0,0 @@
1
- import { promisify } from 'node:util'
2
- import { parseArgs } from 'node:util'
3
- import { glob as _glob } from 'node:fs'
4
-
5
-
6
- const glob = promisify(_glob)
7
-
8
- export async function parseArgsWithGlobs(config) {
9
- const { values, positionals, tokens } = parseArgs({
10
- allowPositionals: true,
11
- ...config,
12
- tokens: true
13
- })
14
-
15
- const files = await globAll(positionals, tokens)
16
-
17
- return { values, positionals, tokens, files }
18
- }
19
-
20
- export async function globAll(arr, tokens) {
21
- const terminatorIndex = findTerminatorIndex(tokens)
22
- const set = new Set()
23
-
24
- if (terminatorIndex >= 0) {
25
- // Glob arguments before the terminator
26
- const beforeTerminator = arr.slice(0, terminatorIndex)
27
- const afterTerminator = arr.slice(terminatorIndex)
28
-
29
- for (const g of beforeTerminator)
30
- for (const file of await glob(g))
31
- set.add(file)
32
-
33
- // Add arguments after terminator as literal strings
34
- for (const literal of afterTerminator)
35
- set.add(literal)
36
- } else {
37
- // No terminator, glob everything (current behavior)
38
- for (const g of arr)
39
- for (const file of await glob(g))
40
- set.add(file)
41
- }
42
-
43
- return Array.from(set)
44
- }
45
-
46
- function findTerminatorIndex(tokens) {
47
- if (!tokens) return -1
48
- for (const token of tokens) {
49
- if (token.kind === 'option-terminator') {
50
- // Find the position in the positionals array
51
- // The terminator itself is not in positionals, so we count preceding positionals
52
- let positionalCount = 0
53
- for (const t of tokens) {
54
- if (t === token) break
55
- if (t.kind === 'positional') positionalCount++
56
- }
57
- return positionalCount
58
- }
59
- }
60
- return -1
61
- }
@@ -1,153 +0,0 @@
1
- import { equal, deepEqual } from 'node:assert/strict'
2
- import test, { describe, before, after } from 'node:test'
3
- import { mkdtemp, writeFile, rm } from 'node:fs/promises'
4
- import { join } from 'node:path'
5
- import { tmpdir } from 'node:os'
6
- import { globAll, parseArgsWithGlobs } from './args-with-globs.js'
7
-
8
-
9
- describe('globAll with token terminator', () => {
10
- let testDir
11
- const testFiles = ['test-file2.png', 'test-file3.png', 'test-file4.png', 'other-file.png']
12
-
13
- before(async () => {
14
- testDir = await mkdtemp(join(tmpdir(), 'glob-test-'))
15
- for (const file of testFiles) {
16
- await writeFile(join(testDir, file), '')
17
- }
18
- })
19
-
20
- after(async () => {
21
- await rm(testDir, { recursive: true, force: true })
22
- })
23
-
24
- test('globs patterns when no terminator present', async () => {
25
- const pattern = join(testDir, 'test-file[234].png')
26
- const result = await globAll([pattern])
27
- const expected = testFiles.slice(0, 3).map(f => join(testDir, f)).sort()
28
- deepEqual(result.sort(), expected)
29
- })
30
-
31
- test('globs patterns before terminator and keeps literals after', async () => {
32
- const pattern = join(testDir, 'test-file[234].png')
33
- const literal = 'my-literal-file[234].png'
34
-
35
- // Simulate tokens from parseArgs with terminator
36
- const tokens = [
37
- { kind: 'positional', index: 0, value: pattern },
38
- { kind: 'option-terminator', index: 1 },
39
- { kind: 'positional', index: 1, value: literal }
40
- ]
41
-
42
- const result = await globAll([pattern, literal], tokens)
43
- const expected = [
44
- ...testFiles.slice(0, 3).map(f => join(testDir, f)),
45
- literal
46
- ].sort()
47
- deepEqual(result.sort(), expected)
48
- })
49
-
50
- test('keeps all arguments literal when terminator is first', async () => {
51
- const literal1 = 'literal-file[234].png'
52
- const literal2 = 'another-file[567].png'
53
-
54
- // Simulate tokens where terminator comes before all positionals
55
- const tokens = [
56
- { kind: 'option-terminator', index: 0 },
57
- { kind: 'positional', index: 0, value: literal1 },
58
- { kind: 'positional', index: 1, value: literal2 }
59
- ]
60
-
61
- const result = await globAll([literal1, literal2], tokens)
62
- deepEqual(result.sort(), [literal1, literal2].sort())
63
- })
64
-
65
- test('handles no terminator with tokens (backwards compat)', async () => {
66
- const pattern = join(testDir, 'test-file[234].png')
67
-
68
- // Tokens without terminator
69
- const tokens = [
70
- { kind: 'positional', index: 0, value: pattern }
71
- ]
72
-
73
- const result = await globAll([pattern], tokens)
74
- const expected = testFiles.slice(0, 3).map(f => join(testDir, f)).sort()
75
- deepEqual(result.sort(), expected)
76
- })
77
-
78
- test('handles undefined tokens (backwards compat)', async () => {
79
- const pattern = join(testDir, 'test-file[234].png')
80
- const result = await globAll([pattern], undefined)
81
- const expected = testFiles.slice(0, 3).map(f => join(testDir, f)).sort()
82
- deepEqual(result.sort(), expected)
83
- })
84
-
85
- test('handles empty arrays', async () => {
86
- const result = await globAll([])
87
- deepEqual(result, [])
88
- })
89
-
90
- test('deduplicates results', async () => {
91
- const pattern = join(testDir, 'test-file2.png')
92
- const result = await globAll([pattern, pattern])
93
- deepEqual(result, [join(testDir, 'test-file2.png')])
94
- })
95
- })
96
-
97
- describe('parseArgsWithGlobs', () => {
98
- let testDir
99
- const testFiles = ['file1.png', 'file2.png', 'file3.png']
100
-
101
- before(async () => {
102
- testDir = await mkdtemp(join(tmpdir(), 'parse-args-test-'))
103
- for (const file of testFiles) {
104
- await writeFile(join(testDir, file), '')
105
- }
106
- })
107
-
108
- after(async () => {
109
- await rm(testDir, { recursive: true, force: true })
110
- })
111
-
112
- test('parses args and globs files in one call', async () => {
113
- const pattern = join(testDir, 'file[12].png')
114
- const { values, files } = await parseArgsWithGlobs({
115
- args: ['--output-dir', '/tmp', pattern],
116
- options: {
117
- 'output-dir': { type: 'string' }
118
- }
119
- })
120
-
121
- equal(values['output-dir'], '/tmp')
122
- deepEqual(files.sort(), [
123
- join(testDir, 'file1.png'),
124
- join(testDir, 'file2.png')
125
- ].sort())
126
- })
127
-
128
- test('respects verbatim token in parseArgsWithGlobs', async () => {
129
- const pattern = join(testDir, 'file[12].png')
130
- const literal = 'literal-file[99].png'
131
- const { files } = await parseArgsWithGlobs({
132
- args: [pattern, '--', literal]
133
- })
134
-
135
- deepEqual(files.sort(), [
136
- join(testDir, 'file1.png'),
137
- join(testDir, 'file2.png'),
138
- literal
139
- ].sort())
140
- })
141
-
142
- test('returns empty files array when no positionals', async () => {
143
- const { files, values } = await parseArgsWithGlobs({
144
- args: ['--flag'],
145
- options: {
146
- flag: { type: 'boolean' }
147
- }
148
- })
149
-
150
- equal(values.flag, true)
151
- deepEqual(files, [])
152
- })
153
- })