mediasnacks 0.7.0 → 0.8.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.
@@ -17,7 +17,7 @@ _mediasnacks_commands=(
17
17
  'rmcover:Removes cover art'
18
18
  'curltime:Measures request response timings',
19
19
  'gif:Video to GIF',
20
- 'flattendir:Moves all files to top dir and deletes dirs'
20
+ 'flattendir:Moves unique files to the top dir and deletes empty dirs'
21
21
  )
22
22
 
23
23
  _mediasnacks() {
package/README.md CHANGED
@@ -32,7 +32,7 @@ Commands:
32
32
  - `curltime`: Measures request response timings
33
33
  - `gif`: Video to GIF
34
34
 
35
- - `flattendir`: Moves all files to top dir and deletes dirs
35
+ - `flattendir`: Moves unique files to the top dir and deletes empty dirs
36
36
  <br/>
37
37
 
38
38
  ### Converting Images to AVIF
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mediasnacks",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
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
@@ -3,7 +3,7 @@
3
3
  import { join } from 'node:path'
4
4
  import { parseArgs } from 'node:util'
5
5
 
6
- import { glob, replaceExt, lstat } from './utils/fs-utils.js'
6
+ import { replaceExt, lstat, globAll } from './utils/fs-utils.js'
7
7
  import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
8
8
 
9
9
 
@@ -15,6 +15,8 @@ Converts images to AVIF.
15
15
 
16
16
 
17
17
  async function main() {
18
+ await assertUserHasFFmpeg()
19
+
18
20
  const { values, positionals } = parseArgs({
19
21
  options: {
20
22
  'output-dir': { type: 'string', default: '' },
@@ -32,22 +34,19 @@ async function main() {
32
34
  if (!positionals.length)
33
35
  throw new Error('No images specified. See npx mediasnacks avif --help')
34
36
 
35
- await assertUserHasFFmpeg()
36
-
37
37
  console.log('AVIF…')
38
- for (const g of positionals)
39
- for (const file of await glob(g))
40
- await toAvif({
41
- file,
42
- outFile: join(values['output-dir'], replaceExt(file, 'avif')),
43
- overwrite: values.overwrite
44
- })
38
+ for (const file of await globAll(positionals))
39
+ await toAvif({
40
+ file,
41
+ outFile: join(values['output-dir'], replaceExt(file, 'avif')),
42
+ overwrite: values.overwrite
43
+ })
45
44
  }
46
45
 
47
46
  async function toAvif({ file, outFile, overwrite }) {
48
47
  const stImg = lstat(file)
49
48
  const stAvif = lstat(outFile)
50
-
49
+
51
50
  if (!overwrite && stAvif?.isFile()) {
52
51
  console.log('(skipped: output file exists but --overwrite=false)', file)
53
52
  return
package/src/cli.js CHANGED
@@ -28,7 +28,7 @@ const COMMANDS = {
28
28
 
29
29
  curltime: ['curltime.sh', 'Measures request response timings'],
30
30
  gif: ['gif.sh', 'Video to GIF'],
31
- flattendir: ['flattendir.py', 'Moves all files to top dir and deletes dirs']
31
+ flattendir: ['flattendir.sh', 'Moves all files to top dir and deletes dirs']
32
32
  }
33
33
 
34
34
  const USAGE = `
package/src/dropdups.js CHANGED
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { spawn } from 'node:child_process'
4
3
  import { parseArgs } from 'node:util'
5
4
  import { resolve, parse, format } from 'node:path'
6
5
 
7
- import { glob } from './utils/fs-utils.js'
6
+ import { globAll } from './utils/fs-utils.js'
8
7
  import { ffmpeg, assertUserHasFFmpeg, run } from './utils/ffmpeg.js'
9
8
 
10
9
 
@@ -34,6 +33,8 @@ Options:
34
33
 
35
34
 
36
35
  async function main() {
36
+ await assertUserHasFFmpeg()
37
+
37
38
  const { values, positionals } = parseArgs({
38
39
  options: {
39
40
  'bad-frame-number': { short: 'n', type: 'string', default: '' },
@@ -54,12 +55,9 @@ async function main() {
54
55
  if (nBadFrame && /^\d+$/.test(nBadFrame))
55
56
  throw new Error('Invalid --bad-frame-number. It must be a positive integer.')
56
57
 
57
- await assertUserHasFFmpeg()
58
-
59
58
  console.log('Dropping Duplicate Frames…')
60
- for (const g of positionals)
61
- for (const file of await glob(g))
62
- await drop(resolve(file), nBadFrame)
59
+ for (const file of await globAll(positionals))
60
+ await drop(resolve(file), nBadFrame)
63
61
  }
64
62
 
65
63
  async function drop(video, nBadFrame) {
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+
3
+ # Moves unique files to the top dir and deletes empty dirs
4
+ # Usage: mediasnacks flattendir [folder]
5
+ # Default: current working directory
6
+
7
+ DIR="${1:-$(pwd)}"
8
+
9
+ find "$DIR" -mindepth 2 -type f | while IFS= read -r file; do
10
+ dest="$DIR/$(basename "$file")"
11
+ if [ ! -e "$dest" ]; then
12
+ mv "$file" "$dest"
13
+ fi
14
+ done
15
+
16
+ find "$DIR" -type f -name '.DS_Store' -delete
17
+ find "$DIR" -depth -type d -empty ! -path "$DIR" -delete
package/src/hev1tohvc1.js CHANGED
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { parseArgs } from 'node:util'
4
- import { unlink, rename } from 'node:fs/promises'
5
4
 
6
- import { glob, uniqueFilenameFor } from './utils/fs-utils.js'
5
+ import { uniqueFilenameFor, overwrite, globAll } from './utils/fs-utils.js'
7
6
  import { videoAttrs, ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
8
7
 
9
8
 
@@ -17,21 +16,19 @@ by changing the container’s sample entry code from HEV1 to HVC1.
17
16
 
18
17
 
19
18
  async function main() {
20
- const { positionals } = parseArgs({ allowPositionals: true })
19
+ await assertUserHasFFmpeg()
21
20
 
21
+ const { positionals } = parseArgs({ allowPositionals: true })
22
22
  if (!positionals.length)
23
23
  throw new Error(USAGE)
24
24
 
25
- await assertUserHasFFmpeg()
26
-
27
25
  console.log('HEV1 to HVC1…')
28
- for (const g of positionals)
29
- for (const file of await glob(g))
30
- await toHvc1(file)
26
+ for (const file of await globAll(positionals))
27
+ await toHvc1(file)
31
28
  }
32
29
 
33
30
  async function toHvc1(file) {
34
- const v = await videoAttrs(file, 'codec_tag_string')
31
+ const v = await videoAttrs(file)
35
32
  if (v.codec_tag_string !== 'hev1') {
36
33
  console.log('(skipped: non hev1)', file)
37
34
  return
@@ -45,8 +42,7 @@ async function toHvc1(file) {
45
42
  '-tag:v', 'hvc1',
46
43
  tmp
47
44
  ])
48
- await unlink(file)
49
- await rename(tmp, file)
45
+ await overwrite(tmp, file)
50
46
  }
51
47
 
52
48
 
package/src/moov2front.js CHANGED
@@ -1,10 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { parseArgs } from 'node:util'
4
- import { unlink, rename } from 'node:fs/promises'
5
4
 
6
- import { glob, uniqueFilenameFor } from './utils/fs-utils.js'
7
5
  import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
6
+ import { uniqueFilenameFor, overwrite, globAll } from './utils/fs-utils.js'
8
7
 
9
8
 
10
9
  const USAGE = `
@@ -16,20 +15,17 @@ Files are overwritten.
16
15
  `.trim()
17
16
 
18
17
  async function main() {
19
- const { positionals } = parseArgs({ allowPositionals: true })
18
+ await assertUserHasFFmpeg()
20
19
 
20
+ const { positionals } = parseArgs({ allowPositionals: true })
21
21
  if (!positionals.length)
22
22
  throw new Error(USAGE)
23
23
 
24
- await assertUserHasFFmpeg()
25
24
  console.log('Optimizing video for progressive download…')
26
-
27
- for (const g of positionals)
28
- for (const file of await glob(g))
29
- await moov2front(file)
25
+ for (const file of await globAll(positionals))
26
+ await moov2front(file)
30
27
  }
31
28
 
32
-
33
29
  async function moov2front(file) {
34
30
  if (!/\.(mp4|mov)$/i.test(file)) {
35
31
  console.log('(skipped: not mp4/mov)', file)
@@ -48,8 +44,7 @@ async function moov2front(file) {
48
44
  '-movflags', '+faststart',
49
45
  tmp
50
46
  ])
51
- await unlink(file)
52
- await rename(tmp, file)
47
+ await overwrite(tmp, file)
53
48
  }
54
49
 
55
50
  async function moovIsBeforeMdat(file) {
package/src/resize.js CHANGED
@@ -4,7 +4,7 @@ import { join } from 'node:path'
4
4
  import { rename } from 'node:fs/promises'
5
5
  import { parseArgs } from 'node:util'
6
6
 
7
- import { glob, isFile, uniqueFilenameFor } from './utils/fs-utils.js'
7
+ import { isFile, uniqueFilenameFor, globAll } from './utils/fs-utils.js'
8
8
  import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/ffmpeg.js'
9
9
 
10
10
 
@@ -28,6 +28,8 @@ Details:
28
28
 
29
29
 
30
30
  async function main() {
31
+ await assertUserHasFFmpeg()
32
+
31
33
  const { values, positionals } = parseArgs({
32
34
  options: {
33
35
  width: { type: 'string', default: '-2' },
@@ -53,23 +55,21 @@ async function main() {
53
55
  if (!positionals.length)
54
56
  throw new Error('No video files specified')
55
57
 
56
- await assertUserHasFFmpeg()
57
58
 
58
59
  console.log('Resizing…')
59
- for (const g of positionals)
60
- for (const file of await glob(g))
61
- await resize({
62
- file,
63
- outFile: join(values['output-dir'], file),
64
- overwrite: values.overwrite,
65
- width,
66
- height,
67
- })
60
+ for (const file of await globAll(positionals))
61
+ await resize({
62
+ file,
63
+ outFile: join(values['output-dir'], file),
64
+ overwrite: values.overwrite,
65
+ width,
66
+ height,
67
+ })
68
68
  }
69
69
 
70
70
 
71
71
  async function resize({ file, outFile, overwrite, width, height }) {
72
- const v = await videoAttrs(file, 'width', 'height')
72
+ const v = await videoAttrs(file)
73
73
  if (width === v.width && height === v.height
74
74
  || width < 0 && height === v.height
75
75
  || height < 0 && width === v.width) {
@@ -15,17 +15,6 @@ export async function assertUserHasFFmpeg() {
15
15
  }
16
16
  }
17
17
 
18
- export async function videoAttrs(v, ...props) {
19
- const { stdout } = await runSilently('ffprobe', [
20
- '-v', 'error',
21
- '-select_streams', 'v:0',
22
- '-show_entries', `stream=${props.join(',')}`,
23
- '-of', 'json',
24
- v
25
- ])
26
- return JSON.parse(stdout).streams[0]
27
- }
28
-
29
18
 
30
19
  async function runSilently(program, args) {
31
20
  return new Promise((resolve, reject) => {
@@ -66,3 +55,94 @@ export async function run(program, args) {
66
55
  }
67
56
 
68
57
 
58
+ /**
59
+ * Describes disposition flags that define how the video stream should be treated
60
+ * by players or downstream consumers.
61
+ * @typedef {Object} VideoStreamDisposition
62
+ * @prop {number} default Is whether this stream is the default selection.
63
+ * @prop {number} dub is dubbed?
64
+ * @prop {number} original is original?
65
+ * @prop {number} comment contains commentary?
66
+ * @prop {number} lyrics has lyrics?
67
+ * @prop {number} karaoke is karaoke?
68
+ * @prop {number} forced must always be rendered?
69
+ * @prop {number} hearing_impaired targets hearing-impaired audiences?
70
+ * @prop {number} visual_impaired targets visually-impaired audiences?
71
+ * @prop {number} clean_effects removes certain effects or noise?
72
+ * @prop {number} attached_pic represents embedded artwork?
73
+ * @prop {number} timed_thumbnails timed thumbnail data?
74
+ * @prop {number} non_diegetic non-diegetic content?
75
+ * @prop {number} captions has captions?
76
+ * @prop {number} descriptions has audio descriptions?
77
+ * @prop {number} metadata has supplemental metadata?
78
+ * @prop {number} dependent depends on another stream?
79
+ * @prop {number} still_image still-image video content?
80
+ * @prop {number} multilayer multilayer stream content?
81
+ */
82
+
83
+ /**
84
+ * Describes metadata tags associated with a video stream.
85
+ * @typedef {Object} VideoStreamTags
86
+ * @prop {string} language stream language.
87
+ * @prop {string} handler_name handler or track label.
88
+ * @prop {string} vendor_id vendor for the encoder or container.
89
+ */
90
+
91
+ /**
92
+ * Full set of attributes returned by ffprobe for a single video stream.
93
+ * @typedef {Object} VideoStream
94
+ * @prop {number} index Numerical index of the stream within the container.
95
+ * @prop {string} codec_name Short codec identifier used by FFmpeg.
96
+ * @prop {string} codec_long_name Descriptive codec name.
97
+ * @prop {string} profile Codec profile used during encoding.
98
+ * @prop {string} codec_type The media type, typically "video".
99
+ * @prop {string} codec_tag_string Codec tag string declared in the container.
100
+ * @prop {string} codec_tag Numeric codec tag in hexadecimal form.
101
+ * @prop {number} width Video width in pixels.
102
+ * @prop {number} height Video height in pixels.
103
+ * @prop {number} coded_width Internal coded width, which may differ from output width.
104
+ * @prop {number} coded_height Internal coded height.
105
+ * @prop {number} has_b_frames Number of B-frames used by the encoder.
106
+ * @prop {string} sample_aspect_ratio Pixel aspect ratio declared in the stream.
107
+ * @prop {string} display_aspect_ratio Display aspect ratio after scaling.
108
+ * @prop {string} pix_fmt Pixel format used by the video stream.
109
+ * @prop {number} level Codec level used during encoding.
110
+ * @prop {string} chroma_location The chroma sample position pattern.
111
+ * @prop {string} field_order Field order (progressive, top-field-first, etc.).
112
+ * @prop {number} refs Number of reference frames used by the encoder.
113
+ * @prop {string} is_avc Indicates whether the stream uses AVC-style NAL units.
114
+ * @prop {string} nal_length_size Length of NAL unit size prefixes.
115
+ * @prop {string} id Stream identifier within the container.
116
+ * @prop {string} r_frame_rate Raw frame rate reported by the demuxer.
117
+ * @prop {string} avg_frame_rate Average frame rate.
118
+ * @prop {string} time_base The fundamental time base of the stream.
119
+ * @prop {number} start_pts Presentation timestamp where the stream begins.
120
+ * @prop {string} start_time Wall-clock start time in seconds.
121
+ * @prop {number} duration_ts Duration expressed in time-base units.
122
+ * @prop {string} duration Stream duration in seconds.
123
+ * @prop {string} bit_rate Declared bit rate of the video stream.
124
+ * @prop {string} bits_per_raw_sample Bit depth of the raw samples.
125
+ * @prop {string} nb_frames Number of frames according to the container.
126
+ * @prop {number} extradata_size Size of the extra codec data.
127
+ * @prop {VideoStreamDisposition} disposition Disposition flags describing playback intent.
128
+ * @prop {VideoStreamTags} tags Metadata tags for the stream.
129
+ */
130
+
131
+ /**
132
+ * Extracts full metadata for the primary video stream (v:0) using ffprobe.
133
+ * @param {string} video Path to the video file.
134
+ * @returns {Promise<VideoStream>} All video stream attributes.
135
+ */
136
+ export async function videoAttrs(v) {
137
+ const { stdout } = await runSilently('ffprobe', [
138
+ '-v', 'error',
139
+ '-select_streams', 'v:0',
140
+ '-show_entries', 'stream',
141
+ '-of', 'json',
142
+ v
143
+ ])
144
+ return JSON.parse(stdout).streams[0]
145
+ }
146
+
147
+
148
+
@@ -1,10 +1,19 @@
1
1
  import { promisify } from 'node:util'
2
2
  import { randomUUID } from 'node:crypto'
3
+ import { unlink, rename } from 'node:fs/promises'
3
4
  import { dirname, extname, join } from 'node:path'
4
5
  import { lstatSync, glob as _glob } from 'node:fs'
5
6
 
6
7
 
7
- export const glob = promisify(_glob)
8
+ const glob = promisify(_glob)
9
+
10
+ export async function globAll(arr) {
11
+ const set = new Set()
12
+ for (const g of arr)
13
+ for (const file of await glob(g))
14
+ set.add(file)
15
+ return Array.from(set)
16
+ }
8
17
 
9
18
  export const lstat = f => lstatSync(f, { throwIfNoEntry: false })
10
19
  export const isFile = path => lstat(path)?.isFile()
@@ -19,3 +28,9 @@ export const replaceExt = (f, ext) => {
19
28
 
20
29
  export const uniqueFilenameFor = file =>
21
30
  join(dirname(file), randomUUID() + extname(file))
31
+
32
+
33
+ export async function overwrite(src, target) {
34
+ await unlink(target)
35
+ await rename(src, target)
36
+ }
package/src/flattendir.py DELETED
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- import shutil
4
- from pathlib import Path
5
-
6
-
7
- def flatten_dir(folder=Path().cwd()):
8
- """Moves all files to top dir and deletes dirs"""
9
-
10
- for path in folder.rglob('*'):
11
- if path.is_file():
12
- dest = folder / path.name
13
- if dest.exists():
14
- dest = folder / f'{path.stem}__{path.stat().st_mtime_ns}{path.suffix}'
15
- shutil.move(str(path), str(dest))
16
-
17
- for sub in sorted(folder.rglob('*'), reverse=True):
18
- if sub.is_dir() and not any(sub.iterdir()):
19
- sub.rmdir()
20
-
21
-
22
- if __name__ == '__main__':
23
- flatten_dir()