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/index.js +4 -0
- package/package.json +1 -1
- package/src/avif.js +17 -29
- package/src/cli.js +18 -21
- package/src/countframes.js +10 -19
- package/src/detectdups.js +10 -17
- package/src/dlaudio.js +34 -0
- package/src/dlvideo.js +28 -0
- package/src/dropdups.js +6 -16
- package/src/edgespic.js +11 -19
- package/src/frameseq.js +13 -21
- package/src/gif.js +43 -0
- package/src/hev1tohvc1.js +10 -21
- package/src/moov2front.js +10 -24
- package/src/openrand.js +4 -10
- package/src/play.js +4 -10
- package/src/png.js +30 -0
- package/src/prores.js +10 -17
- package/src/qdir.js +3 -11
- package/src/resize.js +29 -43
- package/src/seqcheck.js +7 -17
- package/src/sqcrop.js +18 -29
- package/src/ssim.js +4 -12
- package/src/unemoji.js +69 -0
- package/src/utils/fs-utils.js +1 -1
- package/src/utils/parseOptions.js +15 -2
- package/src/utils/subprocess.js +2 -1
- package/src/utils/videoAttrs.js +2 -2
- package/src/vsplit.js +8 -17
- package/src/vtrim.js +7 -15
- package/src/dlaudio.sh +0 -4
- package/src/dlvideo.sh +0 -4
- package/src/gif.sh +0 -44
- package/src/png.sh +0 -20
- package/src/unemoji.sh +0 -30
package/src/moov2front.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ffmpeg
|
|
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
|
-
|
|
20
|
-
|
|
19
|
+
`
|
|
21
20
|
|
|
22
21
|
export default async function main() {
|
|
23
|
-
await
|
|
22
|
+
const { values, files } = await parseOptions(HELP)
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
})
|
|
24
|
+
if (!files.length)
|
|
25
|
+
throw 'Missing input file(s)'
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
49
|
-
if (await moovIsBeforeMdat(file)) throw
|
|
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
|
-
|
|
13
|
+
`
|
|
14
14
|
|
|
15
15
|
export default async function main() {
|
|
16
|
-
|
|
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 (
|
|
25
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
50
|
+
`
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
export default async function main() {
|
|
53
|
-
await
|
|
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'
|
|
58
|
-
end: { short: 'e', type: 'string'
|
|
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
|
|
61
|
+
throw 'Invalid profile. Must be one of: ' + ProresProfiles.list().join(',')
|
|
69
62
|
|
|
70
63
|
if (files.length !== 1)
|
|
71
|
-
throw
|
|
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
|
-
|
|
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
|
|
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
|
|
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 [
|
|
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
|
|
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
|
|
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 (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
67
|
+
throw `no changes needed. ${file}`
|
|
82
68
|
|
|
83
69
|
if (!overwrite && isFile(outFile))
|
|
84
|
-
throw
|
|
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
|
-
|
|
18
|
-
`.trim()
|
|
19
|
-
|
|
18
|
+
`
|
|
20
19
|
|
|
21
|
-
export default function main() {
|
|
22
|
-
const { values, positionals } =
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
16
|
-
|
|
16
|
+
`
|
|
17
17
|
|
|
18
18
|
export default async function main() {
|
|
19
|
-
await
|
|
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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
54
|
-
if (stOut?.mtimeMs > lstat(file)?.mtimeMs) throw
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/utils/fs-utils.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/src/utils/subprocess.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
|
|
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
|
|
package/src/utils/videoAttrs.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
88
|
+
video
|
|
89
89
|
])
|
|
90
90
|
return JSON.parse(stdout).streams?.[0] || {}
|
|
91
91
|
}
|