mediasnacks 0.26.0 → 0.28.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/src/moov2front.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
1
+ import { ffmpeg } from './utils/subprocess.js'
2
2
  import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
3
3
  import { parseOptions } from './utils/parseOptions.js'
4
4
 
@@ -16,37 +16,23 @@ DESCRIPTION
16
16
 
17
17
  NOTES
18
18
  Files are overwritten.
19
- `.trim()
20
-
19
+ `
21
20
 
22
21
  export default async function main() {
23
- await assertUserHasFFmpeg()
22
+ const { values, files } = await parseOptions(HELP)
24
23
 
25
- const { values, files } = await parseOptions({
26
- help: { short: 'h', type: 'boolean' }
27
- })
24
+ if (!files.length)
25
+ throw 'Missing input file(s)'
28
26
 
29
- if (values.help) {
30
- console.log(HELP)
31
- return
27
+ for (const file of files) {
28
+ await moov2front(file)
29
+ console.log(file)
32
30
  }
33
-
34
- if (!files.length) throw new Error('Missing input file(s)')
35
-
36
- console.log('Optimizing video for progressive download…')
37
- for (const file of files)
38
- try {
39
- await moov2front(file)
40
- console.log(file)
41
- }
42
- catch (err) {
43
- console.error(err?.message || err)
44
- }
45
31
  }
46
32
 
47
33
  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}`)
34
+ if (!/\.(mp4|mov)$/i.test(file)) throw `not mp4/mov. ${file}`
35
+ if (await moovIsBeforeMdat(file)) throw `no changes needed. ${file}`
50
36
 
51
37
  const tmp = uniqueFilenameFor(file)
52
38
  await ffmpeg([
package/src/openrand.js CHANGED
@@ -10,21 +10,15 @@ SYNOPSIS
10
10
 
11
11
  DESCRIPTION
12
12
  Opens a random file in the current working directory
13
- `.trim()
13
+ `
14
14
 
15
15
  export default async function main() {
16
- if (process.platform !== 'darwin')
17
- throw new Error('Error: This command is only supported on macOS.')
18
-
19
- const { values } = await parseOptions({
16
+ const { values } = await parseOptions(HELP, {
20
17
  recursive: { short: 'r', type: 'boolean' },
21
- help: { short: 'h', type: 'boolean' }
22
18
  })
23
19
 
24
- if (values.help) {
25
- console.log(HELP)
26
- return
27
- }
20
+ if (process.platform !== 'darwin')
21
+ throw 'This command is only supported on macOS.'
28
22
 
29
23
  openrand('.', values.recursive)
30
24
  }
package/src/play.js CHANGED
@@ -8,25 +8,19 @@ SYNOPSIS
8
8
  mediasnacks play [--no-recursive] [query ...]
9
9
 
10
10
  DESCRIPTION
11
- Plays a filtered playlist with mpv.
11
+ Plays a filtered playlist with mpv
12
12
 
13
13
  EXAMPLE
14
14
  cd Music
15
15
  mediasnacks play artistX artistY
16
- `.trim()
16
+ `
17
17
 
18
18
 
19
19
  export default async function main() {
20
- const { values, positionals } = await parseOptions({
20
+ const { values, positionals } = await parseOptions(HELP, {
21
21
  recursive: { short: 'r', type: 'boolean', default: true },
22
- help: { short: 'h', type: 'boolean' }
23
22
  }, { allowNegative: true })
24
23
 
25
- if (values.help) {
26
- console.log(HELP)
27
- return
28
- }
29
-
30
24
  const files = findFiles({
31
25
  dir: '.',
32
26
  regex: new RegExp(positionals.join('|'), 'i'),
@@ -35,7 +29,7 @@ export default async function main() {
35
29
  })
36
30
 
37
31
  if (!files.length)
38
- throw new Error('No matching files found.')
32
+ throw 'No matching files found.'
39
33
 
40
34
  play(files)
41
35
  }
package/src/png.js ADDED
@@ -0,0 +1,30 @@
1
+ import { parseOptions } from './utils/parseOptions.js'
2
+ import { run } from './utils/subprocess.js'
3
+
4
+
5
+ const HELP = `
6
+ SYNOPSIS
7
+ mediasnacks png <img1> [img2 ...]
8
+
9
+ DESCRIPTION
10
+ Losslessly optimizes PNG images with oxipng at max level.
11
+
12
+ EXAMPLE
13
+ mediasnacks png *.png
14
+ `
15
+
16
+ export default async function main() {
17
+ const { values, files } = await parseOptions(HELP)
18
+
19
+ if (!files.length)
20
+ throw 'Missing input image(s)'
21
+
22
+ await png(...files)
23
+ }
24
+
25
+ export async function png(...images) {
26
+ await run('oxipng', [
27
+ '--opt', 'max',
28
+ ...images
29
+ ])
30
+ }
package/src/prores.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { resolve, parse, join } from 'node:path'
2
2
  import { parseOptions } from './utils/parseOptions.js'
3
- import { assertUserHasFFmpeg, run } from './utils/subprocess.js'
3
+ import { run } from './utils/subprocess.js'
4
4
 
5
+
6
+ // https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
5
7
  export const ProresProfiles = new class {
6
- // https://github.com/oyvindln/vhs-decode/wiki/ProRes-The-Definitive-FFmpeg-Guide#profiles-can-be-the-following
7
8
  profiles = {
8
9
  // 10-bit color depth
9
10
  0: '422 Proxy',
@@ -22,6 +23,7 @@ export const ProresProfiles = new class {
22
23
  table = () => Object.entries(this.profiles)
23
24
  }
24
25
 
26
+
25
27
  const HELP = `
26
28
  SYNOPSIS
27
29
  mediasnacks prores [options] <video>
@@ -33,7 +35,6 @@ OPTIONS
33
35
  -p, --profile <n> Default: ${ProresProfiles.default}
34
36
  -s, --start <time> In time. Unset means beginning
35
37
  -e, --end <time> Out time. Unset means end
36
- -h, --help
37
38
 
38
39
  PROFILES
39
40
  ${ProresProfiles.table().map(([num, name]) =>
@@ -46,29 +47,21 @@ TIME FORMAT
46
47
  EXAMPLES
47
48
  mediasnacks prores --end=60 video.mov // outputs video.prores.mov
48
49
  mediasnacks prores -p2 *.mov
49
- `.trim()
50
+ `
50
51
 
51
52
 
52
53
  export default async function main() {
53
- await assertUserHasFFmpeg()
54
-
55
- const { values, files } = await parseOptions({
54
+ const { values, files } = await parseOptions(HELP, {
56
55
  profile: { short: 'p', type: 'string', default: String(ProresProfiles.default) },
57
- start: { short: 's', type: 'string', default: '' },
58
- end: { short: 'e', type: 'string', default: '' },
59
- help: { short: 'h', type: 'boolean' }
56
+ start: { short: 's', type: 'string' },
57
+ end: { short: 'e', type: 'string' },
60
58
  })
61
59
 
62
- if (values.help) {
63
- console.log(HELP)
64
- return
65
- }
66
-
67
60
  if (!ProresProfiles.isValid(Number(values.profile)))
68
- throw new Error('Invalid profile. Must be one of: ' + ProresProfiles.list().join(','))
61
+ throw 'Invalid profile. Must be one of: ' + ProresProfiles.list().join(',')
69
62
 
70
63
  if (files.length !== 1)
71
- throw new Error('Expected 1 argument: video file. See mediasnacks prores --help')
64
+ throw 'Expected 1 argument: video file.'
72
65
 
73
66
  const video = resolve(files[0])
74
67
  const { name, dir } = parse(video)
package/src/qdir.js CHANGED
@@ -14,8 +14,7 @@ DESCRIPTION
14
14
  Sequentially runs all *.sh files in a folder (cwd by default).
15
15
  Completed scripts get renamed with a ".done" extension,
16
16
  or to ".failed.$exitCode"
17
- `.trim()
18
-
17
+ `
19
18
 
20
19
  function filter(f) {
21
20
  return f.endsWith('.sh')
@@ -29,19 +28,12 @@ function newExt(exitCode) {
29
28
 
30
29
 
31
30
  export default async function main() {
32
- const { values, positionals } = await parseOptions({
33
- help: { short: 'h', type: 'boolean' }
34
- })
35
-
36
- if (values.help) {
37
- console.log(HELP)
38
- return
39
- }
31
+ const { values, positionals } = await parseOptions(HELP)
40
32
 
41
33
  const dir = positionals[0] || process.cwd()
42
34
  const err = await qdir(dir)
43
35
  if (err)
44
- throw new Error(err)
36
+ throw err
45
37
  }
46
38
 
47
39
 
package/src/resize.js CHANGED
@@ -3,72 +3,58 @@ import { rename } from 'node:fs/promises'
3
3
 
4
4
  import { parseOptions } from './utils/parseOptions.js'
5
5
  import { isFile, uniqueFilenameFor } from './utils/fs-utils.js'
6
- import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
6
+ import { ffmpeg } from './utils/subprocess.js'
7
7
  import { videoAttrs } from './utils/videoAttrs.js'
8
8
 
9
9
 
10
10
  const HELP = `
11
11
  SYNOPSIS
12
- mediasnacks resize [--width=<num>] [--height=<num>] [-y | --overwrite] [--outdir=<dir>] <files>
12
+ mediasnacks resize [options] <files>
13
13
 
14
14
  DESCRIPTION
15
15
  Resizes videos and images. The aspect ratio is preserved when only one dimension is specified.
16
16
 
17
+ OPTIONS
18
+ --width <num>
19
+ --height <num>
20
+ Only one is needed. They are -2 by default:
21
+ -1 = auto-compute while preserving the aspect ratio (may result in an odd number)
22
+ -2 = same as -1 but rounds to the nearest even number
23
+ --outdir <dir> Defaults to same dir of input file
24
+ -y, --overwrite
25
+
17
26
  EXAMPLES
18
27
  Overwrites the input file (-y)
19
28
  mediasnacks resize -y --width 480 'dir-a/**/*.png' 'dir-b/**/*.mp4'
20
29
 
21
- Output directory (-o)
30
+ Output directory
22
31
  mediasnacks resize --height 240 --outdir /tmp/out video.mov
23
-
24
- OPTIONS
25
- --width and --height are -2 by default:
26
- -1 = auto-compute while preserving the aspect ratio (may result in an odd number)
27
- -2 = same as -1 but rounds to the nearest even number
28
- `.trim()
29
-
32
+ `
30
33
 
31
34
  export default async function main() {
32
- await assertUserHasFFmpeg()
33
-
34
- const { values, files } = await parseOptions({
35
+ const { values, files } = await parseOptions(HELP, {
35
36
  width: { type: 'string', default: '-2' },
36
37
  height: { type: 'string', default: '-2' },
37
38
  outdir: { type: 'string', default: '' },
38
39
  overwrite: { short: 'y', type: 'boolean' },
39
- help: { short: 'h', type: 'boolean' },
40
40
  })
41
41
 
42
- if (values.help) {
43
- console.log(HELP)
44
- return
45
- }
46
-
47
42
  const width = Number(values.width)
48
43
  const height = Number(values.height)
49
44
 
50
- if (width <= 0 && height <= 0)
51
- throw new Error('--width or --height need to be greater than 0')
52
-
53
- if (!files.length)
54
- throw new Error('No video files specified')
55
-
56
-
57
- console.log('Resizing…')
58
- for (const file of files)
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
- }
45
+ if (!files.length) throw 'No video files specified'
46
+ if (width <= 0 && height <= 0) throw '--width or --height must be > 0'
47
+
48
+ for (const file of files) {
49
+ await resize({
50
+ file,
51
+ outFile: join(values.outdir, file),
52
+ overwrite: values.overwrite,
53
+ width,
54
+ height,
55
+ })
56
+ console.log(file)
57
+ }
72
58
  }
73
59
 
74
60
 
@@ -78,10 +64,10 @@ export async function resize({ file, outFile, overwrite, width, height }) {
78
64
  || width < 0 && height === v.height
79
65
  || height < 0 && width === v.width
80
66
  )
81
- throw new Error(`no changes needed. ${file}`)
67
+ throw `no changes needed. ${file}`
82
68
 
83
69
  if (!overwrite && isFile(outFile))
84
- throw new Error(`output file exists but --overwrite=false. ${file}`)
70
+ throw `output file exists but --overwrite=false. ${file}`
85
71
 
86
72
  const tmp = uniqueFilenameFor(file)
87
73
  await ffmpeg([
package/src/seqcheck.js CHANGED
@@ -1,5 +1,6 @@
1
- import { parseArgs } from 'node:util'
2
1
  import { readdirSync } from 'node:fs'
2
+ import { parseOptions } from './utils/parseOptions.js'
3
+
3
4
 
4
5
  const LEFT_DELIM = '_'
5
6
  const RIGHT_DELIM = '.'
@@ -14,25 +15,14 @@ DESCRIPTION
14
15
  OPTIONS
15
16
  -ld, --left-delimiter <str> Delimiter before the number (default: "${LEFT_DELIM}")
16
17
  -rd, --right-delimiter <str> Delimiter after the number (default: "${RIGHT_DELIM}")
17
- -h, --help
18
- `.trim()
19
-
18
+ `
20
19
 
21
- export default function main() {
22
- const { values, positionals } = parseArgs({
23
- options: {
24
- 'left-delimiter': { type: 'string', default: LEFT_DELIM },
25
- 'right-delimiter': { type: 'string', default: RIGHT_DELIM },
26
- help: { short: 'h', type: 'boolean' },
27
- },
28
- allowPositionals: true,
20
+ export default async function main() {
21
+ const { values, positionals } = await parseOptions(HELP, {
22
+ 'left-delimiter': { type: 'string', default: LEFT_DELIM },
23
+ 'right-delimiter': { type: 'string', default: RIGHT_DELIM },
29
24
  })
30
25
 
31
- if (values.help) {
32
- console.log(HELP)
33
- return
34
- }
35
-
36
26
  const dir = positionals[0] || process.cwd()
37
27
  const missing = seqcheck(dir, values['left-delimiter'], values['right-delimiter'])
38
28
  if (missing.length)
package/src/sqcrop.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { join } from 'node:path'
2
2
  import { rename } from 'node:fs/promises'
3
3
 
4
- import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
4
+ import { ffmpeg } from './utils/subprocess.js'
5
5
  import { lstat, uniqueFilenameFor } from './utils/fs-utils.js'
6
6
  import { parseOptions } from './utils/parseOptions.js'
7
+ import { videoAttrs } from './utils/videoAttrs.js'
7
8
 
8
9
 
9
10
  const HELP = `
@@ -12,46 +13,34 @@ SYNOPSIS
12
13
 
13
14
  DESCRIPTION
14
15
  Square crops images
15
- `.trim()
16
-
16
+ `
17
17
 
18
18
  export default async function main() {
19
- await assertUserHasFFmpeg()
20
-
21
- const { values, files } = await parseOptions({
19
+ const { values, files } = await parseOptions(HELP, {
22
20
  outdir: { type: 'string', default: '' },
23
21
  overwrite: { short: 'y', type: 'boolean' },
24
- help: { short: 'h', type: 'boolean' },
25
22
  })
26
23
 
27
- if (values.help) {
28
- console.log(HELP)
29
- return
30
- }
31
-
32
24
  if (!files.length)
33
- throw new Error('No images specified. See mediasnacks sqcrop --help')
34
-
35
- console.log('Cropping…')
36
- for (const file of files)
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
- }
25
+ throw 'No images specified'
26
+
27
+ for (const file of files) {
28
+ await sqcrop({
29
+ file,
30
+ outFile: join(values.outdir, file),
31
+ overwrite: values.overwrite
32
+ })
33
+ console.log(file)
34
+ }
48
35
  }
49
36
 
50
37
  export async function sqcrop({ file, outFile, overwrite }) {
51
38
  const stOut = lstat(outFile)
39
+ const { width, height } = await videoAttrs(file)
52
40
 
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}`)
41
+ if (!overwrite && stOut?.isFile()) throw `output file exists. ${file}`
42
+ if (stOut?.mtimeMs > lstat(file)?.mtimeMs) throw `output file is newer. ${file}`
43
+ if (width === height) throw `already square. ${file}`
55
44
 
56
45
  const tmp = uniqueFilenameFor(file)
57
46
  await ffmpeg([
package/src/ssim.js CHANGED
@@ -8,21 +8,13 @@ SYNOPSIS
8
8
 
9
9
  DESCRIPTION
10
10
  Computes the Structural Similarity Index (SSIM) between two images using FFmpeg.
11
- `.trim()
12
-
11
+ `
13
12
 
14
13
  export default async function main() {
15
- const { values, positionals } = await parseOptions({
16
- help: { short: 'h', type: 'boolean' }
17
- })
18
-
19
- if (values.help) {
20
- console.log(HELP)
21
- return
22
- }
14
+ const { values, positionals } = await parseOptions(HELP)
23
15
 
24
16
  if (positionals.length !== 2)
25
- throw new Error('Expected two images')
17
+ throw 'Expected two images'
26
18
 
27
19
  const score = await ssim(...positionals)
28
20
  console.log(score.toString())
@@ -37,6 +29,6 @@ export async function ssim(img1, img2) {
37
29
  ])
38
30
  const match = stderr.match(/All:([\d.]+)/)
39
31
  if (!match)
40
- throw new Error(`Could not parse SSIM output:\n${stderr}`)
32
+ throw `Could not parse SSIM output:\n${stderr}`
41
33
  return parseFloat(match[1])
42
34
  }
package/src/unemoji.js ADDED
@@ -0,0 +1,69 @@
1
+ import { rename } from 'node:fs/promises'
2
+ import { existsSync } from 'node:fs'
3
+ import { dirname, basename, join } from 'node:path'
4
+ import { parseOptions } from './utils/parseOptions.js'
5
+ import { findFiles } from './utils/fs-utils.js'
6
+
7
+
8
+ const HELP = `
9
+ SYNOPSIS
10
+ mediasnacks unemoji [-r | --recursive] <dir>
11
+
12
+ DESCRIPTION
13
+ Removes emoji from filenames in the current directory.
14
+ Does not overwrite files.
15
+ `
16
+
17
+ const EMOJI_RE = new RegExp(
18
+ '[' +
19
+ '\u{1F600}-\u{1F64F}' + // Emoticons
20
+ '\u{1F300}-\u{1F5FF}' + // Misc Symbols and Pictographs
21
+ '\u{1F680}-\u{1F6FF}' + // Transport and Map
22
+ '\u{2600}-\u{26FF}' + // Misc symbols
23
+ '\u{2700}-\u{27BF}' + // Dingbats
24
+ '\u{1F900}-\u{1F9FF}' + // Supplemental Symbols and Pictographs
25
+ '\u{1FA70}-\u{1FAFF}' + // Symbols and Pictographs Extended-A
26
+ '\u{1F1E6}-\u{1F1FF}' + // Regional Indicator Symbols
27
+ ']',
28
+ 'gu'
29
+ )
30
+
31
+ export default async function main() {
32
+ const { values, positionals } = await parseOptions(HELP, {
33
+ recursive: { short: 'r', type: 'boolean' }
34
+ })
35
+
36
+ if (positionals.length !== 1)
37
+ throw 'Must pass only one dir'
38
+
39
+ const files = findFiles({
40
+ dir: positionals[0],
41
+ regex: EMOJI_RE,
42
+ recursive: values.recursive,
43
+ })
44
+
45
+ for (const file of files) {
46
+ const newpath = await unemoji(file)
47
+ if (newpath)
48
+ console.log(`Renaming: ${file} -> ${newpath}`)
49
+ }
50
+ }
51
+
52
+ export async function unemoji(file) {
53
+ const dir = dirname(file)
54
+ const base = basename(file)
55
+ const newbase = base.replace(EMOJI_RE, '')
56
+ .normalize('NFKC')
57
+ .replace(/\s+/g, ' ')
58
+ .replace(/\s+\./g, '.')
59
+ .trim()
60
+ if (base === newbase)
61
+ return null
62
+
63
+ const newpath = join(dir, newbase)
64
+ if (existsSync(newpath))
65
+ throw `Skipping (exists): ${file} -> ${newpath}`
66
+
67
+ await rename(file, newpath)
68
+ return newpath
69
+ }
@@ -34,7 +34,7 @@ export async function mkDir(path) {
34
34
  }
35
35
  }
36
36
 
37
- export function findFiles({ dir, regex, recursive, ignoredDirs }) {
37
+ export function findFiles({ dir, regex, recursive, ignoredDirs = [] }) {
38
38
  return readdirSync(dir, { withFileTypes: true, recursive })
39
39
  .filter(entry =>
40
40
  entry.isFile()
@@ -1,10 +1,17 @@
1
1
  import { promisify, parseArgs } from 'node:util'
2
2
  import { glob as _glob } from 'node:fs'
3
3
 
4
-
5
4
  const glob = promisify(_glob)
6
5
 
7
- export async function parseOptions(options = {}, config = {}) {
6
+
7
+ /**
8
+ * @param {string} helpText
9
+ * @param {import('node:util').ParseArgsOptionsConfig} [options]
10
+ * @param {Partial<import('node:util').ParseArgsConfig>} [config]
11
+ */
12
+ export async function parseOptions(helpText, options = {}, config = {}) {
13
+ options.help = { short: 'h', type: 'boolean' }
14
+
8
15
  const { values, positionals, tokens } = parseArgs({
9
16
  args: process.argv.slice(3),
10
17
  allowPositionals: true,
@@ -12,6 +19,12 @@ export async function parseOptions(options = {}, config = {}) {
12
19
  ...config,
13
20
  tokens: true
14
21
  })
22
+
23
+ if (values.help) {
24
+ console.log(helpText.trim())
25
+ process.exit(0)
26
+ }
27
+
15
28
  return {
16
29
  values,
17
30
  positionals,
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'node:child_process'
2
2
 
3
3
 
4
- export async function assertUserHasFFmpeg() {
4
+ async function assertUserHasFFmpeg() {
5
5
  try {
6
6
  await runSilently('ffmpeg', ['-version'])
7
7
  await runSilently('ffprobe', ['-version'])
@@ -12,6 +12,7 @@ export async function assertUserHasFFmpeg() {
12
12
  }
13
13
 
14
14
  export async function ffmpeg(args) {
15
+ await assertUserHasFFmpeg()
15
16
  return runSilently('ffmpeg', args)
16
17
  }
17
18
 
@@ -79,13 +79,13 @@ import { runSilently } from './subprocess.js'
79
79
  * @param {string} video Path to the video file.
80
80
  * @returns {Promise<VideoStream>} All video stream attributes.
81
81
  */
82
- export async function videoAttrs(v) {
82
+ export async function videoAttrs(video) {
83
83
  const { stdout } = await runSilently('ffprobe', [
84
84
  '-v', 'error',
85
85
  '-select_streams', 'v:0',
86
86
  '-show_entries', 'stream',
87
87
  '-of', 'json',
88
- v
88
+ video
89
89
  ])
90
90
  return JSON.parse(stdout).streams?.[0] || {}
91
91
  }