mediasnacks 0.20.1 → 0.22.1

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
@@ -13,7 +13,7 @@ npm install -g mediasnacks
13
13
  Optionally, if you have `ignore-scripts=true` in your `.npmprc`,
14
14
  you can install zsh auto-completions with:
15
15
  ```sh
16
- $(npm root -g)/mediasnacks/install-zsh-completions.sh
16
+ $(npm root -g)/mediasnacks/install-zsh-completions.js
17
17
  ```
18
18
 
19
19
 
@@ -50,6 +50,8 @@ mediasnacks <command> <args>
50
50
  - `flattendir`: Moves unique files to the top dir and deletes empty dirs
51
51
  - `qdir` Sequentially runs all *.sh files in a folder
52
52
  - `seqcheck` Finds missing sequence number
53
+ - `random` Opens a random file
54
+ - `play` Plays filtered playlist with mpv
53
55
 
54
56
 
55
57
  - `dlaudio`: yt-dlp best audio
@@ -60,8 +62,6 @@ mediasnacks <command> <args>
60
62
  - `rmcover`: Removes cover art
61
63
 
62
64
 
63
- - `curltime`: Measures request response timings
64
-
65
65
  ### Globs
66
66
  Glob patterns are expanded by Node.js.
67
67
 
@@ -76,57 +76,6 @@ mediasnacks avif -- file[234].png
76
76
  ```
77
77
 
78
78
 
79
- ---
80
-
81
- ## Commands
82
-
83
- ### Converting Images to AVIF
84
- ```shell
85
- mediasnacks avif [-y | --overwrite] [--output-dir=<dir>] <images>
86
- ```
87
-
88
- <br/>
89
-
90
- ### Resizing Images or Videos
91
- Resizes videos and images. The aspect ratio is preserved when only one dimension is specified.
92
-
93
- `--width` and `--height` are `-2` by default:
94
- - `-1` auto-compute while preserving the aspect ratio (may result in an odd number)
95
- - `-2` same as `-1` but rounds to the nearest even number
96
-
97
- ```shell
98
- mediasnacks resize [--width=<num>] [--height=<num>] [-y | --overwrite] [--output-dir=<dir>] <files>
99
- ```
100
-
101
- Example: Overwrites the input file (-y)
102
- ```shell
103
- mediasnacks resize -y --width 480 'dir-a/**/*.png' 'dir-b/**/*.mp4'
104
- ```
105
-
106
- Example: Output directory (-o)
107
- ```shell
108
- mediasnacks resize --height 240 --output-dir /tmp/out video.mov
109
- ```
110
-
111
- <br/>
112
-
113
- ### Fast-Start Streaming Video
114
- Rearranges .mov and .mp4 metadata to the start of the file for fast-start streaming.
115
-
116
- **Files are overwritten**
117
-
118
- ```shell
119
- mediasnacks moov2front <videos>
120
- ```
121
- What is Fast Start?
122
- - https://wiki.avblocks.com/avblocks-for-cpp/muxer-parameters/mp4
123
- - https://trac.ffmpeg.org/wiki/HowToCheckIfFaststartIsEnabledForPlayback
124
-
125
-
126
- <br/>
127
-
128
- ---
129
-
130
79
  ## Adding a macOS Quick Action
131
80
 
132
81
 
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { join } from 'node:path'
4
+ import { execSync } from 'node:child_process'
5
+ import { writeFileSync } from 'node:fs'
6
+ import { commandsSummary } from './src/cli.js'
7
+
8
+ let zshFuncDefsDirs
9
+ try {
10
+ zshFuncDefsDirs = execSync('zsh -c "print -l \\$fpath"', { encoding: 'utf-8' })
11
+ }
12
+ catch {
13
+ process.exit(0) // Exit on systems without ZSH
14
+ }
15
+
16
+ for (const dir of zshFuncDefsDirs.split('\n'))
17
+ try {
18
+ writeFileSync(join(dir, '_mediasnacks'), makeScript(), { mode: 0o755 })
19
+ break
20
+ }
21
+ catch {}
22
+
23
+
24
+ function makeScript() {
25
+ return `#compdef mediasnacks
26
+
27
+ _mediasnacks_commands=(
28
+ ${commandsSummary().map(([cmd, desc]) => `'${cmd}:${desc}'`).join('\n')}
29
+ )
30
+
31
+ if (( CURRENT == 2 )); then
32
+ _describe -t commands 'mediasnacks commands' _mediasnacks_commands
33
+ return
34
+ fi
35
+
36
+ local cmd="$words[2]"
37
+ case "$cmd" in
38
+ qdir)
39
+ _files -/
40
+ ;;
41
+ *)
42
+ _files
43
+ ;;
44
+ esac
45
+ `
46
+ }
47
+
48
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.20.1",
3
+ "version": "0.22.1",
4
4
  "description": "Utilities for optimizing and preparing videos and images",
5
5
  "license": "MIT",
6
6
  "author": "Eric Fortis",
@@ -10,12 +10,11 @@
10
10
  },
11
11
  "scripts": {
12
12
  "test": "docker run --rm $(docker build -q .)",
13
- "postinstall": "sh install-zsh-completions.sh",
13
+ "postinstall": "node install-zsh-completions.js",
14
14
  "dev-install": "npm i -g . --ignore-scripts=false"
15
15
  },
16
16
  "files": [
17
17
  "src",
18
- ".zsh",
19
- "install-zsh-completions.sh"
18
+ "install-zsh-completions.js"
20
19
  ]
21
20
  }
package/src/avif.js CHANGED
@@ -4,10 +4,10 @@ import { join, basename, dirname } from 'node:path'
4
4
 
5
5
  import { parseOptions } from './utils/parseOptions.js'
6
6
  import { replaceExt, lstat } from './utils/fs-utils.js'
7
- import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
7
+ import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
8
8
 
9
9
 
10
- const MAN = `
10
+ const HELP = `
11
11
  SYNOPSIS
12
12
  mediasnacks avif [-y | --overwrite] [--output-dir=<dir>] <images>
13
13
 
@@ -26,7 +26,7 @@ async function main() {
26
26
  })
27
27
 
28
28
  if (values.help) {
29
- console.log(MAN)
29
+ console.log(HELP)
30
30
  process.exit(0)
31
31
  }
32
32
 
@@ -35,14 +35,14 @@ async function main() {
35
35
 
36
36
  console.log('AVIF…')
37
37
  for (const file of files)
38
- await toAvif({
38
+ await avif({
39
39
  file,
40
40
  outFile: join(values['output-dir'] || dirname(file), replaceExt(basename(file), 'avif')),
41
41
  overwrite: values.overwrite
42
42
  })
43
43
  }
44
44
 
45
- async function toAvif({ file, outFile, overwrite }) {
45
+ async function avif({ file, outFile, overwrite }) {
46
46
  const stAvif = lstat(outFile)
47
47
 
48
48
  if (!overwrite && stAvif?.isFile()) {
package/src/cli.js CHANGED
@@ -18,7 +18,7 @@ const COMMANDS = {
18
18
  detectdups: ['detectdups.js', 'Detects duplicate frames in a video'],
19
19
  dropdups: ['dropdups.js', 'Removes duplicate frames in a video'],
20
20
  framediff: ['framediff.sh', 'Plays a video of adjacent frames diff'],
21
- hev1tohvc1: ['hev1tohvc1.js', 'Fixes video thumbnails not rendering in macOS Finder '],
21
+ hev1tohvc1: ['hev1tohvc1.js', 'Fixes video thumbnails not rendering on macOS Finder'],
22
22
  moov2front: ['moov2front.js', 'Rearranges .mov and .mp4 metadata for fast-start streaming'],
23
23
  vconcat: ['vconcat.sh', 'Concatenates videos'],
24
24
  vdiff: ['vdiff.sh', 'Plays a video with the difference of two videos'],
@@ -28,47 +28,57 @@ const COMMANDS = {
28
28
 
29
29
  flattendir: ['flattendir.sh', 'Moves all files to top dir and deletes dirs'],
30
30
  qdir: ['qdir.js', 'Sequentially runs all *.sh files in a folder'],
31
- seqcheck: ['seqcheck.js', 'Finds missing sequence number\n'],
31
+ seqcheck: ['seqcheck.js', 'Finds missing sequence number'],
32
+ random: ['random.js', 'Opens a random file (macOS only)'],
33
+ play: ['play.js', 'Plays filtered playlist with mpv\n'],
32
34
 
33
35
  dlaudio: ['dlaudio.sh', 'yt-dlp best audio'],
34
36
  dlvideo: ['dlvideo.sh', 'yt-dlp best video\n'],
35
37
 
36
38
  unemoji: ['unemoji.sh', 'Removes emojis from filenames'],
37
- rmcover: ['rmcover.sh', 'Removes cover art\n'],
39
+ rmcover: ['rmcover.sh', 'Removes cover art'],
40
+ }
38
41
 
39
- curltime: ['curltime.sh', 'Measures request response timings'],
42
+ export function commandsSummary() {
43
+ return Object.entries(COMMANDS)
44
+ .map(([cmd, [, desc]]) => [cmd, desc])
40
45
  }
41
46
 
42
- const MAN = `
47
+ const HELP = `
43
48
  SYNOPSIS
44
49
  mediasnacks <command> <args>
45
50
 
46
51
  COMMANDS
47
- ${Object.entries(COMMANDS).map(([cmd, [, title]]) =>
48
- ` ${styleText('bold', cmd.padEnd(12, ' '))}\t${title}`).join('\n')}
52
+ ${commandsSummary().map(([cmd, desc]) =>
53
+ ` ${styleText('bold', cmd.padEnd(12, ' '))}\t${desc}`).join('\n')}
49
54
  `.trim()
50
55
 
51
56
 
52
- const [, , opt, ...args] = process.argv
53
-
54
- if (opt === '-v' || opt === '--version') {
55
- console.log(pkgJSON.version)
56
- process.exit(0)
57
- }
58
- if (opt === '-h' || opt === '--help') {
59
- console.log(MAN)
60
- process.exit(0)
61
- }
62
-
63
- if (!opt) {
64
- console.log(MAN)
65
- process.exit(1)
66
- }
67
- if (!Object.hasOwn(COMMANDS, opt)) {
68
- console.error(`'${opt}' is not a command. See mediasnacks --help\n`)
69
- process.exit(1)
57
+ function main() {
58
+ const [, , opt, ...args] = process.argv
59
+
60
+ if (opt === '-v' || opt === '--version') {
61
+ console.log(pkgJSON.version)
62
+ process.exit(0)
63
+ }
64
+ if (opt === '-h' || opt === '--help') {
65
+ console.log(HELP)
66
+ process.exit(0)
67
+ }
68
+
69
+ if (!opt) {
70
+ console.log(HELP)
71
+ process.exit(1)
72
+ }
73
+ if (!Object.hasOwn(COMMANDS, opt)) {
74
+ console.error(`'${opt}' is not a command. See mediasnacks --help\n`)
75
+ process.exit(1)
76
+ }
77
+
78
+ const cmd = join(import.meta.dirname, COMMANDS[opt][0])
79
+ spawn(cmd, args, { stdio: 'inherit' })
80
+ .on('exit', process.exit)
70
81
  }
71
82
 
72
- const cmd = join(import.meta.dirname, COMMANDS[opt][0])
73
- spawn(cmd, args, { stdio: 'inherit' })
74
- .on('exit', process.exit)
83
+ if (import.meta.main)
84
+ main()
package/src/detectdups.js CHANGED
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { parseOptions } from './utils/parseOptions.js'
4
- import { ffmpeg, assertUserHasFFmpeg, videoAttrs } from './utils/ffmpeg.js'
4
+ import { ffmpeg, assertUserHasFFmpeg, videoAttrs } from './utils/subprocess.js'
5
5
 
6
6
  const STDEV_THRESHOLD = 0.2
7
7
 
8
- const MAN = `
9
- Usage: mediasnacks detectdups [options] <video>
8
+ const HELP = `
9
+ SYNOPSIS
10
+ mediasnacks detectdups [options] <video>
10
11
 
11
- Detects sequentially duplicate frames in a video and prints a histogram of their distance.
12
+ DESCRIPTION
13
+ Detects sequentially duplicate frames in a video and prints a histogram of their distance.
12
14
 
13
15
  EXAMPLES
14
16
  Peak at N=2, means that every other frame is repeated, such as in a
@@ -38,7 +40,7 @@ async function main() {
38
40
  })
39
41
 
40
42
  if (values.help) {
41
- console.log(MAN)
43
+ console.log(HELP)
42
44
  process.exit(0)
43
45
  }
44
46
 
@@ -70,11 +72,20 @@ async function main() {
70
72
  throw new Error(`Invalid analysis range. Exceeds video duration: ${vDur}`)
71
73
 
72
74
 
73
- const dups = await detectDuplicateFramesNums(files[0], seek, duration)
74
- analyze(dups, seek, duration)
75
+ const dups = await detectdups(files[0], seek, duration)
76
+ const h = deltaHistogram(dups)
77
+ const report = {
78
+ n: maxFreqKey(h),
79
+ histogram: h,
80
+ analyzed_region: {
81
+ start_sec: seek,
82
+ end_sec: seek + duration
83
+ },
84
+ }
85
+ console.log(JSON.stringify(report, null, 2))
75
86
  }
76
87
 
77
- export async function detectDuplicateFramesNums(video, seek, duration) {
88
+ export async function detectdups(video, seek, duration) {
78
89
  const { stderr } = await ffmpeg([
79
90
  '-v', 'info',
80
91
  '-stats',
@@ -106,29 +117,22 @@ export async function detectDuplicateFramesNums(video, seek, duration) {
106
117
 
107
118
  // This is only good for when there's one repeated frame in a cycle.
108
119
  // i.e. it's the wrong approach for e.g. 25 to 60, in which N=2 and N=3
109
- function analyze(dup_frames, seek, duration) {
120
+ function deltaHistogram(dups) {
110
121
  const histogram = {}
111
- for (let i = 1; i < dup_frames.length; i++) {
112
- const diff = dup_frames[i] - dup_frames[i - 1]
122
+ for (let i = 1; i < dups.length; i++) {
123
+ const diff = dups[i] - dups[i - 1]
113
124
  histogram[diff] = (histogram[diff] || 0) + 1
114
125
  }
115
- console.log(JSON.stringify({
116
- analyzed_region: {
117
- start_sec: seek,
118
- end_sec: seek + duration
119
- },
120
- histogram,
121
- n: maxFreqKey(histogram)
122
- }, null, 2))
126
+ return histogram
123
127
  }
124
128
 
125
- function maxFreqKey(hist) {
129
+ function maxFreqKey(histogram) {
126
130
  let maxKey = null
127
131
  let maxVal = -1
128
- for (const [key, val] of Object.entries(hist))
129
- if (val > maxVal) {
130
- maxVal = val
131
- maxKey = key
132
+ for (const [k, v] of Object.entries(histogram))
133
+ if (v > maxVal) {
134
+ maxVal = v
135
+ maxKey = k
132
136
  }
133
137
  return maxKey !== null
134
138
  ? Number(maxKey)
@@ -136,7 +140,6 @@ function maxFreqKey(hist) {
136
140
  }
137
141
 
138
142
 
139
-
140
143
  if (import.meta.main)
141
144
  main().catch(err => {
142
145
  console.error(err.message || err)
@@ -6,31 +6,19 @@ import { cli } from './utils/test-utils.js'
6
6
  const rel = f => join(import.meta.dirname, f)
7
7
 
8
8
  function detect(video) {
9
- const { stdout } = cli('detectdups', rel(video), 0, 7)
9
+ const { stdout } = cli('detectdups', rel(video))
10
10
  return JSON.parse(stdout).n
11
11
  }
12
12
 
13
-
14
- test('no dups', () =>
15
- equal(detect('fixtures/big-buck-bunny/bbb_24fps_no_dups.mp4'), null))
16
-
13
+ test('no dups', () => equal(detect('fixtures/big-buck-bunny/bbb_24fps_no_dups.mp4'), null))
17
14
 
18
15
  // These fixtures are badly retimed (non-interpolated, just duplicating a frame)
19
16
 
20
- test('24 to 48 (has dup at n=2)', () =>
21
- equal(detect('fixtures/big-buck-bunny/bbb_24_to_48fps_dup.mp4'), 2))
22
-
23
- test('25 to 50 (has dup at n=2)', () =>
24
- equal(detect('fixtures/big-buck-bunny/bbb_25_to_50fps_dup.mp4'), 2))
25
-
26
-
27
- test('24 to 30 (has dup at n=5)', () =>
28
- equal(detect('fixtures/big-buck-bunny/bbb_24_to_30fps_dup.mp4'), 5))
29
-
30
- test('25 to 30 (has dup at n=6)', () =>
31
- equal(detect('fixtures/big-buck-bunny/bbb_25_to_30fps_dup.mp4'), 6))
17
+ test('24 to 48 (has dup at n=2)', () => equal(detect('fixtures/big-buck-bunny/bbb_24_to_48fps_dup.mp4'), 2))
18
+ test('25 to 50 (has dup at n=2)', () => equal(detect('fixtures/big-buck-bunny/bbb_25_to_50fps_dup.mp4'), 2))
32
19
 
20
+ test('24 to 30 (has dup at n=5)', () => equal(detect('fixtures/big-buck-bunny/bbb_24_to_30fps_dup.mp4'), 5))
21
+ test('25 to 30 (has dup at n=6)', () => equal(detect('fixtures/big-buck-bunny/bbb_25_to_30fps_dup.mp4'), 6))
33
22
 
34
- test('24 to 25 (has dup at n=25)', () =>
35
- equal(detect('fixtures/big-buck-bunny/bbb_24_to_25fps_dup.mp4'), 25))
23
+ test('24 to 25 (has dup at n=25)', () => equal(detect('fixtures/big-buck-bunny/bbb_24_to_25fps_dup.mp4'), 25))
36
24
 
package/src/dropdups.js CHANGED
@@ -3,21 +3,13 @@
3
3
  import { resolve, parse, format } from 'node:path'
4
4
 
5
5
  import { parseOptions } from './utils/parseOptions.js'
6
- import { ffmpeg, assertUserHasFFmpeg, run } from './utils/ffmpeg.js'
6
+ import { ffmpeg, assertUserHasFFmpeg, run } from './utils/subprocess.js'
7
+ import { PRORES_PROFILES } from './prores.js'
7
8
 
8
9
 
9
- const PRORES_PROFILES = {
10
- 'proxy': 0,
11
- 'lt': 1,
12
- 'standard': 2,
13
- 'hq': 3,
14
- '4444': 4,
15
- '4444xq': 5,
16
- }
17
10
  const PROFILE = PRORES_PROFILES.hq
18
11
 
19
-
20
- const MAN = `
12
+ const HELP = `
21
13
  SYNOPSIS
22
14
  mediasnacks dropdups [-n <bad-frame-number>] <video>
23
15
 
@@ -42,7 +34,7 @@ async function main() {
42
34
  })
43
35
 
44
36
  if (values.help) {
45
- console.log(MAN)
37
+ console.log(HELP)
46
38
  process.exit(0)
47
39
  }
48
40
 
@@ -55,10 +47,10 @@ async function main() {
55
47
 
56
48
  console.log('Dropping Duplicate Frames…')
57
49
  for (const file of files)
58
- await drop(resolve(file), nBadFrame)
50
+ await dropdups(resolve(file), nBadFrame)
59
51
  }
60
52
 
61
- async function drop(video, nBadFrame) {
53
+ async function dropdups(video, nBadFrame) {
62
54
  await run('ffmpeg', [
63
55
  '-v', 'error',
64
56
  '-stats',
package/src/edgespic.js CHANGED
@@ -4,10 +4,10 @@ import { basename, extname, join, parse } from 'node:path'
4
4
 
5
5
  import { mkDir } from './utils/fs-utils.js'
6
6
  import { parseOptions } from './utils/parseOptions.js'
7
- import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/ffmpeg.js'
7
+ import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/subprocess.js'
8
8
 
9
9
 
10
- const MAN = `
10
+ const HELP = `
11
11
  SYNOPSIS
12
12
  mediasnacks edgespic [--width=<num>] <files>
13
13
 
@@ -30,7 +30,7 @@ async function main() {
30
30
  })
31
31
 
32
32
  if (values.help) {
33
- console.log(MAN)
33
+ console.log(HELP)
34
34
  process.exit(0)
35
35
  }
36
36
 
@@ -46,11 +46,11 @@ async function main() {
46
46
 
47
47
  console.log('Extracting edge frames…')
48
48
  for (const file of files)
49
- await extractEdgeFrames(file, width, outDir)
49
+ await edgespic(file, width, outDir)
50
50
  }
51
51
 
52
52
 
53
- async function extractEdgeFrames(video, width, outDir) {
53
+ async function edgespic(video, width, outDir) {
54
54
  const { r_frame_rate } = await videoAttrs(video)
55
55
  const name = basename(video, extname(video))
56
56
 
package/src/hev1tohvc1.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { parseOptions } from './utils/parseOptions.js'
4
4
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
5
- import { videoAttrs, ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
5
+ import { videoAttrs, ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
6
6
 
7
7
 
8
- const MAN = `
8
+ const HELP = `
9
9
  SYNOPSIS
10
10
  mediasnacks hev1tohvc1 <videos>
11
11
 
@@ -22,14 +22,14 @@ async function main() {
22
22
  const { files } = await parseOptions()
23
23
 
24
24
  if (!files.length)
25
- throw new Error(MAN)
25
+ throw new Error(HELP)
26
26
 
27
27
  console.log('HEV1 to HVC1…')
28
28
  for (const file of files)
29
- await toHvc1(file)
29
+ await hev1tohvc1(file)
30
30
  }
31
31
 
32
- async function toHvc1(file) {
32
+ async function hev1tohvc1(file) {
33
33
  const v = await videoAttrs(file)
34
34
  if (v.codec_tag_string !== 'hev1') {
35
35
  console.log('(skipped: non hev1)', file)
package/src/moov2front.js CHANGED
@@ -1,16 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
3
+ import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
4
4
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
5
5
  import { parseOptions } from './utils/parseOptions.js'
6
6
 
7
7
 
8
- const MAN = `
8
+ const HELP = `
9
9
  SYNOPSIS
10
10
  mediasnacks moov2front <videos>
11
11
 
12
12
  DESCRIPTION
13
13
  Rearranges .mov and .mp4 metadata to the start of the file for fast-start streaming.
14
+
15
+ What is Fast Start?
16
+ - https://wiki.avblocks.com/avblocks-for-cpp/muxer-parameters/mp4
17
+ - https://trac.ffmpeg.org/wiki/HowToCheckIfFaststartIsEnabledForPlayback
14
18
 
15
19
  NOTES
16
20
  Files are overwritten.
@@ -22,7 +26,7 @@ async function main() {
22
26
  const { files } = await parseOptions()
23
27
 
24
28
  if (!files.length)
25
- throw new Error(MAN)
29
+ throw new Error(HELP)
26
30
 
27
31
  console.log('Optimizing video for progressive download…')
28
32
  for (const file of files)
package/src/play.js ADDED
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ import { join } from 'node:path'
3
+ import { spawn } from 'node:child_process'
4
+ import { readdirSync } from 'node:fs'
5
+ import { parseOptions } from './utils/parseOptions.js'
6
+
7
+
8
+ const HELP = `
9
+ SYNOPSIS
10
+ mediasnacks play [--no-recursive] [-h | --help] [query ...]
11
+
12
+ DESCRIPTION
13
+ Plays a filtered playlist with mpv.
14
+
15
+ EXAMPLE
16
+ cd Music
17
+ mediasnacks play artistX artistY
18
+ `.trim()
19
+
20
+
21
+ async function main() {
22
+ const { values, positionals } = await parseOptions({
23
+ recursive: { short: 'r', type: 'boolean', default: true },
24
+ help: { short: 'h', type: 'boolean' }
25
+ }, { allowNegative: true })
26
+
27
+ if (values.help) {
28
+ console.log(HELP)
29
+ process.exit(0)
30
+ }
31
+
32
+ const pattern = positionals.length
33
+ ? positionals.join('|')
34
+ : ''
35
+ const files = findFiles('.', new RegExp(pattern, 'i'), values.recursive)
36
+
37
+ if (!files.length) {
38
+ console.error('No matching files found.')
39
+ process.exit(0)
40
+ }
41
+
42
+ const child = spawn('mpv', ['--playlist=-'], {
43
+ detached: true,
44
+ stdio: ['pipe', 'ignore', 'ignore']
45
+ })
46
+ child.stdin.end(files.join('\n'))
47
+ child.unref()
48
+ }
49
+
50
+ function findFiles(dir, regex, recursive = true) {
51
+ const IGNORED_DIRS = ['.fcpbundle/']
52
+ return readdirSync(dir, { withFileTypes: true, recursive })
53
+ .filter(entry =>
54
+ entry.isFile()
55
+ && !entry.name.startsWith('.')
56
+ && !IGNORED_DIRS.some(d => entry.parentPath.includes(d))
57
+ && regex.test(entry.name))
58
+ .map(entry => join(entry.parentPath, entry.name))
59
+ }
60
+
61
+ main().catch(err => {
62
+ console.error(err.message)
63
+ process.exit(1)
64
+ })
package/src/prores.js CHANGED
@@ -3,9 +3,9 @@
3
3
  import { resolve, parse, join } from 'node:path'
4
4
 
5
5
  import { parseOptions } from './utils/parseOptions.js'
6
- import { assertUserHasFFmpeg, run } from './utils/ffmpeg.js'
6
+ import { assertUserHasFFmpeg, run } from './utils/subprocess.js'
7
7
 
8
- const PRORES_PROFILES = {
8
+ export const PRORES_PROFILES = {
9
9
  'proxy': 0,
10
10
  'lt': 1,
11
11
  'standard': 2,
@@ -14,7 +14,7 @@ const PRORES_PROFILES = {
14
14
  '4444xq': 5,
15
15
  }
16
16
 
17
- const MAN = `
17
+ const HELP = `
18
18
  SYNOPSIS
19
19
  mediasnacks prores [options] <video>
20
20
 
@@ -42,31 +42,35 @@ async function main() {
42
42
  })
43
43
 
44
44
  if (values.help) {
45
- console.log(MAN)
45
+ console.log(HELP)
46
46
  process.exit(0)
47
47
  }
48
48
 
49
49
  if (files.length !== 1)
50
50
  throw new Error('Expected 1 argument: video file. See mediasnacks prores --help')
51
51
 
52
- const videoPath = resolve(files[0])
53
-
54
- const { name, dir } = parse(videoPath)
55
- const outputPath = join(dir, `${name}.prores.mov`)
52
+ const video = resolve(files[0])
53
+ const { name, dir } = parse(video)
54
+ const output = join(dir, `${name}.prores.mov`)
56
55
 
57
56
  console.log(`Converting to ProRes…`)
57
+ await prores(video, values.profile, output)
58
+ }
59
+
60
+ async function prores(video, profile, output) {
58
61
  await run('ffmpeg', [
59
62
  '-v', 'error',
60
63
  '-stats',
61
- '-i', videoPath,
64
+ '-i', video,
62
65
  '-c:v', 'prores_ks',
63
- '-profile:v', values.profile,
66
+ '-profile:v', profile,
64
67
  '-pix_fmt', 'yuv422p10le',
65
- outputPath
68
+ output
66
69
  ])
67
70
  }
68
71
 
69
- main().catch(err => {
70
- console.error(err.message || err)
71
- process.exit(1)
72
- })
72
+ if (import.meta.main)
73
+ main().catch(err => {
74
+ console.error(err.message || err)
75
+ process.exit(1)
76
+ })
package/src/qdir.js CHANGED
@@ -8,7 +8,7 @@ import { readdir, writeFile, unlink, rename } from 'node:fs/promises'
8
8
  import { isFile } from './utils/fs-utils.js'
9
9
 
10
10
 
11
- const MAN = `
11
+ const HELP = `
12
12
  SYNOPSIS
13
13
  mediasnacks qdir [folder]
14
14
 
@@ -26,7 +26,7 @@ async function main() {
26
26
  })
27
27
 
28
28
  if (values.help) {
29
- console.log(MAN)
29
+ console.log(HELP)
30
30
  process.exit(0)
31
31
  }
32
32
 
package/src/random.js ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ import { join } from 'node:path'
3
+ import { spawn } from 'node:child_process'
4
+ import { readdirSync } from 'node:fs'
5
+ import { parseOptions } from './utils/parseOptions.js'
6
+
7
+
8
+ const HELP = `
9
+ SYNOPSIS
10
+ mediasnacks random [-r | --recursive] [-h | --help]
11
+
12
+ DESCRIPTION
13
+ Opens a random file in the current working directory
14
+ `.trim()
15
+
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
+ }
21
+
22
+ const { values } = await parseOptions({
23
+ recursive: { short: 'r', type: 'boolean' },
24
+ help: { short: 'h', type: 'boolean' }
25
+ })
26
+
27
+ if (values.help) {
28
+ console.log(HELP)
29
+ process.exit(0)
30
+ }
31
+
32
+ spawn('open', [pickRandomFile('.', values.recursive)])
33
+ }
34
+
35
+ function pickRandomFile(dir, recursive) {
36
+ const files = readdirSync(dir, { withFileTypes: true, recursive })
37
+ .filter(entry => !entry.name.startsWith('.') && entry.isFile())
38
+ .map(entry => join(entry.parentPath, entry.name))
39
+ return files[Math.floor(Math.random() * files.length)]
40
+ }
41
+
42
+ main().catch(err => {
43
+ console.error(err.message)
44
+ process.exit(1)
45
+ })
package/src/resize.js CHANGED
@@ -5,10 +5,10 @@ import { rename } from 'node:fs/promises'
5
5
 
6
6
  import { parseOptions } from './utils/parseOptions.js'
7
7
  import { isFile, uniqueFilenameFor } from './utils/fs-utils.js'
8
- import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/ffmpeg.js'
8
+ import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/subprocess.js'
9
9
 
10
10
 
11
- const MAN = `
11
+ const HELP = `
12
12
  SYNOPSIS
13
13
  mediasnacks resize [--width=<num>] [--height=<num>] [-y | --overwrite] [--output-dir=<dir>] <files>
14
14
 
@@ -41,7 +41,7 @@ async function main() {
41
41
  })
42
42
 
43
43
  if (values.help) {
44
- console.log(MAN)
44
+ console.log(HELP)
45
45
  process.exit(0)
46
46
  }
47
47
 
package/src/seqcheck.js CHANGED
@@ -4,7 +4,7 @@ import { parseArgs } from 'node:util'
4
4
  import { readdirSync } from 'node:fs'
5
5
 
6
6
 
7
- const MAN = `
7
+ const HELP = `
8
8
  SYNOPSIS
9
9
  mediasnacks seqcheck [options] [folder]
10
10
 
@@ -29,7 +29,7 @@ function main() {
29
29
  })
30
30
 
31
31
  if (values.help) {
32
- console.log(MAN)
32
+ console.log(HELP)
33
33
  process.exit(0)
34
34
  }
35
35
 
package/src/sqcrop.js CHANGED
@@ -3,12 +3,12 @@
3
3
  import { join } from 'node:path'
4
4
  import { rename } from 'node:fs/promises'
5
5
 
6
- import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
6
+ import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
7
7
  import { lstat, uniqueFilenameFor } from './utils/fs-utils.js'
8
8
  import { parseOptions } from './utils/parseOptions.js'
9
9
 
10
10
 
11
- const MAN = `
11
+ const HELP = `
12
12
  SYNOPSIS
13
13
  mediasnacks sqcrop [-y | --overwrite] [--output-dir=<dir>] <images>
14
14
 
@@ -27,7 +27,7 @@ async function main() {
27
27
  })
28
28
 
29
29
  if (values.help) {
30
- console.log(MAN)
30
+ console.log(HELP)
31
31
  process.exit(0)
32
32
  }
33
33
 
package/src/ssim.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { ffmpeg } from './utils/ffmpeg.js'
3
+ import { ffmpeg } from './utils/subprocess.js'
4
4
 
5
5
 
6
- const MAN = `
6
+ const HELP = `
7
7
  SYNOPSIS
8
8
  mediasnacks ssim <img1> <img2>
9
9
 
@@ -15,7 +15,7 @@ DESCRIPTION
15
15
  async function main() {
16
16
  const [img1, img2] = process.argv.slice(2)
17
17
  if (!img1 || !img2) {
18
- console.log(MAN)
18
+ console.log(HELP)
19
19
  process.exit(1)
20
20
  }
21
21
 
@@ -13,6 +13,7 @@ export async function parseOptions(options = {}, config = {}) {
13
13
  })
14
14
  return {
15
15
  values,
16
+ positionals,
16
17
  files: await resolveGlobs(positionals, tokens)
17
18
  }
18
19
  }
@@ -21,12 +21,13 @@ describe('parseOptions', () => {
21
21
  after(() => rm(testDir, { recursive: true }))
22
22
 
23
23
  test('parses args and globs files', async () => {
24
- const { values, files } = await parseOptions({
24
+ const { values, positionals, files } = await parseOptions({
25
25
  'output-dir': { type: 'string' }
26
26
  }, {
27
27
  args: ['--output-dir', '/tmp', inTmpDir('file[12].png')],
28
28
  })
29
29
  equal(values['output-dir'], '/tmp')
30
+ deepEqual(positionals, [inTmpDir('file[12].png')])
30
31
  deepEqual(files, [
31
32
  inTmpDir('file1.png'),
32
33
  inTmpDir('file2.png')
@@ -3,7 +3,7 @@ import { join } from 'node:path'
3
3
  import { test } from 'node:test'
4
4
  import { cpSync } from 'node:fs'
5
5
 
6
- import { videoAttrs } from './utils/ffmpeg.js'
6
+ import { videoAttrs } from './utils/subprocess.js'
7
7
  import { mkTempDir, cli } from './utils/test-utils.js'
8
8
 
9
9
  const rel = f => join(import.meta.dirname, f)
package/src/vsplit.js CHANGED
@@ -4,12 +4,12 @@ import { readFileSync } from 'node:fs'
4
4
  import { resolve, parse, join } from 'node:path'
5
5
 
6
6
  import { parseOptions } from './utils/parseOptions.js'
7
- import { assertUserHasFFmpeg, run } from './utils/ffmpeg.js'
7
+ import { assertUserHasFFmpeg, run } from './utils/subprocess.js'
8
8
 
9
9
 
10
10
  // TODO looks like it's missing a frame (perhaps becaue of -c copy)
11
11
 
12
- const MAN = `
12
+ const HELP = `
13
13
  SYNOPSIS
14
14
  mediasnacks vsplit <csv> <video>
15
15
 
@@ -41,7 +41,7 @@ async function main() {
41
41
  })
42
42
 
43
43
  if (values.help) {
44
- console.log(MAN)
44
+ console.log(HELP)
45
45
  process.exit(0)
46
46
  }
47
47
 
@@ -55,7 +55,7 @@ async function main() {
55
55
  throw new Error('CSV file contains no clips')
56
56
 
57
57
  console.log(`Splitting video into ${clips.length} clip${clips.length === 1 ? '' : 's'}…`)
58
- await splitVideo(videoPath, clips)
58
+ await vsplit(videoPath, clips)
59
59
  }
60
60
 
61
61
  function parseCSV(csvPath) {
@@ -77,7 +77,7 @@ function parseCSV(csvPath) {
77
77
  return clips
78
78
  }
79
79
 
80
- async function splitVideo(videoPath, clips) {
80
+ async function vsplit(videoPath, clips) {
81
81
  const { dir, name, ext } = parse(videoPath)
82
82
  const seqLen = Math.log10(clips.length) + 1 | 0
83
83
 
@@ -3,7 +3,7 @@ import { join } from 'node:path'
3
3
  import { describe, test } from 'node:test'
4
4
  import { cpSync, readdirSync } from 'node:fs'
5
5
 
6
- import { videoAttrs } from './utils/ffmpeg.js'
6
+ import { videoAttrs } from './utils/subprocess.js'
7
7
  import { mkTempDir, cli } from './utils/test-utils.js'
8
8
 
9
9
 
package/src/vtrim.js CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  import { resolve, parse } from 'node:path'
4
4
  import { parseOptions } from './utils/parseOptions.js'
5
- import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
5
+ import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
6
6
 
7
7
 
8
- const MAN = `
8
+ const HELP = `
9
9
  SYNOPSIS
10
10
  mediasnacks vtrim [--start <time>] [--end <time>] <video>
11
11
 
@@ -29,7 +29,7 @@ async function main() {
29
29
  })
30
30
 
31
31
  if (values.help) {
32
- console.log(MAN)
32
+ console.log(HELP)
33
33
  process.exit(0)
34
34
  }
35
35
 
@@ -37,10 +37,10 @@ async function main() {
37
37
  throw new Error('No video specified. See mediasnacks vtrim --help')
38
38
 
39
39
  for (const file of files)
40
- await trim(resolve(file), values.start, values.end)
40
+ await vtrim(resolve(file), values.start, values.end)
41
41
  }
42
42
 
43
- async function trim(video, start, end) {
43
+ async function vtrim(video, start, end) {
44
44
  const { dir, name, ext } = parse(video)
45
45
  await ffmpeg([
46
46
  '-v', 'error',
package/src/vtrim.test.js CHANGED
@@ -3,7 +3,7 @@ import { join } from 'node:path'
3
3
  import { cpSync } from 'node:fs'
4
4
  import { describe, test } from 'node:test'
5
5
 
6
- import { videoAttrs } from './utils/ffmpeg.js'
6
+ import { videoAttrs } from './utils/subprocess.js'
7
7
  import { mkTempDir, cli } from './utils/test-utils.js'
8
8
 
9
9
  const rel = f => join(import.meta.dirname, f)
@@ -1,49 +0,0 @@
1
- #compdef mediasnacks
2
-
3
- _mediasnacks_commands=(
4
- 'avif:Converts images to AVIF'
5
- 'sqcrop:Square crops images'
6
-
7
- 'resize:Resizes videos or images'
8
- 'edgespic:Extracts first and last frames'
9
- 'ssim:Computes similarity of two images'
10
- 'gif:Video to GIF'
11
-
12
- 'detectdups:Detects sequentially duplicate frames in a video'
13
- 'dropdups:Removes sequentially duplicate frames in a video'
14
- 'framediff:ffplay with a filter for diffing adjacent frames'
15
- 'hev1tohvc1:Fixes video thumbnails not rendering in macOS Finder'
16
- 'moov2front:Rearranges metadata for fast-start streaming'
17
- 'vconcat:Concatenates videos'
18
- 'vdiff:Plays a video with the difference of two videos'
19
- 'vsplit:Splits a video into multiple clips from CSV timestamps'
20
- 'vtrim:Trims video from start to end time'
21
- 'prores:Converts video to Apple ProRes'
22
-
23
- 'flattendir:Moves unique files to the top dir and deletes empty dirs'
24
- 'seqcheck:Finds missing sequence number'
25
- 'qdir:Sequentially runs all *.sh files in a folder'
26
-
27
- 'dlaudio: yt-dlp best audio'
28
- 'dlvideo: yt-dlp best video'
29
-
30
- 'unemoji:Removes emojis from filenames'
31
- 'rmcover:Removes cover art'
32
-
33
- 'curltime:Measures request response timings'
34
- )
35
-
36
- if (( CURRENT == 2 )); then
37
- _describe -t commands 'mediasnacks commands' _mediasnacks_commands
38
- return
39
- fi
40
-
41
- local cmd="$words[2]"
42
- case "$cmd" in
43
- avif|resize|sqcrop|moov2front|detectdups|dropdups|edgespic|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|vsplit|vtrim|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|flattendir|prores|ssim)
44
- _files
45
- ;;
46
- qdir)
47
- _files -/
48
- ;;
49
- esac
@@ -1,23 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
-
4
- # Exit on systems without ZSH
5
- zsh=$(command -v zsh) || exit 0
6
-
7
- src="$(cd "$(dirname "$0")" && pwd)/.zsh/completions/_mediasnacks"
8
- [ -f "$src" ] || exit 0
9
-
10
- exec "$zsh" -s "$src" << 'ZSH_EOF'
11
- src="$1"
12
- for dir in "${fpath[@]}"; do
13
- if [ -w "$dir" ]; then
14
- dst="$dir/_mediasnacks"
15
- ln -sf "$src" "$dst"
16
- echo "linked zsh completions: $dst -> $src"
17
- exit 0
18
- fi
19
- done
20
- echo "zsh completions: no writable fpath directory found, skipping." >&2
21
- exit 0
22
- ZSH_EOF
23
-
package/src/curltime.sh DELETED
@@ -1,14 +0,0 @@
1
- #!/bin/sh
2
-
3
- # https://stackoverflow.com/a/47944496
4
-
5
- curl -so /dev/null -w "\
6
- DNS Lookup %{time_namelookup}
7
- TCP Handshake %{time_connect}
8
- TLS Handshake %{time_appconnect}
9
- Wait %{time_pretransfer}
10
- Redirect %{time_redirect}
11
- First Byte %{time_starttransfer}
12
- ───────────────────────
13
- TOTAL %{time_total}
14
- " "$@"
File without changes