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/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export { avif } from './src/avif.js'
|
|
2
2
|
export { countframes } from './src/countframes.js'
|
|
3
3
|
export { detectdups } from './src/detectdups.js'
|
|
4
|
+
export { dlaudio } from './src/dlaudio.js'
|
|
5
|
+
export { dlvideo } from './src/dlvideo.js'
|
|
4
6
|
export { dropdups } from './src/dropdups.js'
|
|
5
7
|
export { edgespic } from './src/edgespic.js'
|
|
6
8
|
export { frameseq } from './src/frameseq.js'
|
|
7
9
|
export { hev1tohvc1 } from './src/hev1tohvc1.js'
|
|
8
10
|
export { moov2front } from './src/moov2front.js'
|
|
11
|
+
export { png } from './src/png.js'
|
|
9
12
|
export { play } from './src/play.js'
|
|
10
13
|
export { prores } from './src/prores.js'
|
|
11
14
|
export { qdir } from './src/qdir.js'
|
|
@@ -14,5 +17,6 @@ export { resize } from './src/resize.js'
|
|
|
14
17
|
export { seqcheck } from './src/seqcheck.js'
|
|
15
18
|
export { sqcrop } from './src/sqcrop.js'
|
|
16
19
|
export { ssim } from './src/ssim.js'
|
|
20
|
+
export { unemoji } from './src/unemoji.js'
|
|
17
21
|
export { vsplit } from './src/vsplit.js'
|
|
18
22
|
export { vtrim } from './src/vtrim.js'
|
package/package.json
CHANGED
package/src/avif.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join, basename, dirname } from 'node:path'
|
|
2
2
|
import { parseOptions } from './utils/parseOptions.js'
|
|
3
3
|
import { replaceExt, lstat } from './utils/fs-utils.js'
|
|
4
|
-
import { ffmpeg
|
|
4
|
+
import { ffmpeg } from './utils/subprocess.js'
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
const HELP = `
|
|
@@ -14,45 +14,33 @@ DESCRIPTION
|
|
|
14
14
|
EXAMPLES
|
|
15
15
|
mediasnacks avif -y '*.png'
|
|
16
16
|
mediasnacks avif --outdir=foo/ 'a/**/*.png'
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
`
|
|
19
18
|
|
|
20
19
|
export default async function main() {
|
|
21
|
-
await
|
|
22
|
-
|
|
23
|
-
const { values, files } = await parseOptions({
|
|
20
|
+
const { values, files } = await parseOptions(HELP, {
|
|
24
21
|
outdir: { type: 'string', default: '' },
|
|
25
22
|
overwrite: { short: 'y', type: 'boolean' },
|
|
26
|
-
help: { short: 'h', type: 'boolean' },
|
|
27
23
|
})
|
|
28
24
|
|
|
29
|
-
if (values.help) {
|
|
30
|
-
console.log(HELP)
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
25
|
if (!files.length)
|
|
35
|
-
throw
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
console.log(file)
|
|
46
|
-
}
|
|
47
|
-
catch (err) {
|
|
48
|
-
console.error(err?.message || err)
|
|
49
|
-
}
|
|
26
|
+
throw 'Invalid input image'
|
|
27
|
+
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
await avif({
|
|
30
|
+
file,
|
|
31
|
+
outFile: join(values.outdir || dirname(file), replaceExt(basename(file), 'avif')),
|
|
32
|
+
overwrite: values.overwrite
|
|
33
|
+
})
|
|
34
|
+
console.log(file)
|
|
35
|
+
}
|
|
50
36
|
}
|
|
51
37
|
|
|
52
38
|
export async function avif({ file, outFile, overwrite = false }) {
|
|
53
39
|
const stAvif = lstat(outFile)
|
|
54
|
-
if (!overwrite
|
|
55
|
-
|
|
40
|
+
if (!overwrite) {
|
|
41
|
+
if (stAvif?.isFile()) throw `output file exists: ${file}`
|
|
42
|
+
if (stAvif?.mtimeMs > lstat(file)?.mtimeMs) throw `avif is newer: ${file}`
|
|
43
|
+
}
|
|
56
44
|
|
|
57
45
|
await ffmpeg([
|
|
58
46
|
'-y',
|
package/src/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import pkgJSON from '../package.json' with { type: 'json' }
|
|
|
8
8
|
|
|
9
9
|
const COMMANDS = {
|
|
10
10
|
avif: ['./avif.js', 'Converts images to AVIF'],
|
|
11
|
-
png: ['./png.
|
|
11
|
+
png: ['./png.js', 'Optimizes PNG images with oxipng'],
|
|
12
12
|
sqcrop: ['./sqcrop.js', 'Square crops images\n'],
|
|
13
13
|
|
|
14
14
|
resize: ['./resize.js', 'Resizes videos or images'],
|
|
@@ -16,7 +16,7 @@ const COMMANDS = {
|
|
|
16
16
|
frameseq: ['./frameseq.js', 'Converts video to sequence of PNGs'],
|
|
17
17
|
countframes: ['./countframes.js', 'Counts frames in a video'],
|
|
18
18
|
ssim: ['./ssim.js', 'Computes SSIM between two images'],
|
|
19
|
-
gif: ['./gif.
|
|
19
|
+
gif: ['./gif.js', 'Video to GIF\n'],
|
|
20
20
|
|
|
21
21
|
detectdups: ['./detectdups.js', 'Detects duplicate frames in a video'],
|
|
22
22
|
dropdups: ['./dropdups.js', 'Removes duplicate frames in a video'],
|
|
@@ -35,10 +35,10 @@ const COMMANDS = {
|
|
|
35
35
|
openrand: ['./openrand.js', 'Opens a random file (macOS only)'],
|
|
36
36
|
play: ['./play.js', 'Plays filtered playlist with mpv\n'],
|
|
37
37
|
|
|
38
|
-
dlaudio: ['./dlaudio.
|
|
39
|
-
dlvideo: ['./dlvideo.
|
|
38
|
+
dlaudio: ['./dlaudio.js', 'yt-dlp best audio'],
|
|
39
|
+
dlvideo: ['./dlvideo.js', 'yt-dlp best video\n'],
|
|
40
40
|
|
|
41
|
-
unemoji: ['./unemoji.
|
|
41
|
+
unemoji: ['./unemoji.js', 'Removes emojis from filenames'],
|
|
42
42
|
rmcover: ['./rmcover.sh', 'Removes cover art'],
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -60,27 +60,24 @@ ${commandsSummary().map(([cmd, desc]) =>
|
|
|
60
60
|
async function main() {
|
|
61
61
|
const [, , opt, ...args] = process.argv
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
console.log(HELP)
|
|
69
|
-
return
|
|
70
|
-
}
|
|
63
|
+
switch (opt) {
|
|
64
|
+
case '-v':
|
|
65
|
+
case '--version':
|
|
66
|
+
console.log(pkgJSON.version)
|
|
67
|
+
return
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (!Object.hasOwn(COMMANDS, opt)) {
|
|
77
|
-
console.error(`'${opt}' is not a command. See mediasnacks --help\n`)
|
|
78
|
-
process.exit(1)
|
|
69
|
+
case '-h':
|
|
70
|
+
case '--help':
|
|
71
|
+
console.log(HELP)
|
|
72
|
+
return
|
|
79
73
|
}
|
|
80
74
|
|
|
75
|
+
if (!opt) throw HELP
|
|
76
|
+
if (!Object.hasOwn(COMMANDS, opt)) throw `'${opt}' is not a command. See mediasnacks --help\n`
|
|
77
|
+
|
|
81
78
|
const cmd = COMMANDS[opt][0]
|
|
82
79
|
if (cmd.endsWith('.js'))
|
|
83
|
-
(await import(cmd)).default()
|
|
80
|
+
await (await import(cmd)).default()
|
|
84
81
|
else
|
|
85
82
|
spawn(join(import.meta.dirname, cmd), args, { stdio: 'inherit' })
|
|
86
83
|
.on('exit', process.exit)
|
package/src/countframes.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { parseOptions } from './utils/parseOptions.js'
|
|
2
|
-
import { assertUserHasFFmpeg } from './utils/subprocess.js'
|
|
3
2
|
import { videoAttrs } from './utils/videoAttrs.js'
|
|
4
3
|
import { parseTimecode } from './utils/parseTimecode.js'
|
|
5
4
|
|
|
@@ -19,27 +18,20 @@ OPTIONS
|
|
|
19
18
|
EXAMPLES
|
|
20
19
|
mediasnacks countframes --start=1:30.16 --end=60 video.mov
|
|
21
20
|
mediasnacks countframes --fps=12 video.mov
|
|
22
|
-
|
|
21
|
+
`
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
export default async function main() {
|
|
26
|
-
await
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
start: { short: 's', type: 'string', default: '' },
|
|
31
|
-
end: { short: 'e', type: 'string', default: '' },
|
|
32
|
-
help: { short: 'h', type: 'boolean' }
|
|
25
|
+
const { values, files } = await parseOptions(HELP, {
|
|
26
|
+
fps: { type: 'string' },
|
|
27
|
+
start: { short: 's', type: 'string' },
|
|
28
|
+
end: { short: 'e', type: 'string' },
|
|
33
29
|
})
|
|
34
30
|
|
|
35
|
-
if (values.help) {
|
|
36
|
-
console.log(HELP)
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
|
|
40
31
|
const { fps, start, end } = values
|
|
41
32
|
const video = files[0]
|
|
42
|
-
if (!video)
|
|
33
|
+
if (!video)
|
|
34
|
+
throw 'No video file specified'
|
|
43
35
|
|
|
44
36
|
const n = await countframes({ video, fps, start, end })
|
|
45
37
|
console.log(String(n))
|
|
@@ -48,10 +40,9 @@ export default async function main() {
|
|
|
48
40
|
|
|
49
41
|
export async function countframes({ video, fps, start, end }) {
|
|
50
42
|
const v = await videoAttrs(video)
|
|
51
|
-
const
|
|
43
|
+
const duration = parseFloat(v.duration || 0)
|
|
52
44
|
const startSecs = start ? parseTimecode(start) : 0
|
|
53
|
-
const endSecs = end ? parseTimecode(end) :
|
|
54
|
-
const durationLimit = Math.max(0, endSecs - startSecs)
|
|
45
|
+
const endSecs = end ? parseTimecode(end) : duration
|
|
55
46
|
const actualFps = fps ? Number(fps) : eval(v.r_frame_rate)
|
|
56
|
-
return Math.ceil(
|
|
47
|
+
return Math.ceil(Math.max(0, endSecs - startSecs) * actualFps)
|
|
57
48
|
}
|
package/src/detectdups.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parseOptions } from './utils/parseOptions.js'
|
|
2
|
-
import { ffmpeg
|
|
2
|
+
import { ffmpeg } from './utils/subprocess.js'
|
|
3
3
|
import { videoAttrs } from './utils/videoAttrs.js'
|
|
4
4
|
|
|
5
5
|
const STDEV_THRESHOLD = 0.2
|
|
@@ -22,32 +22,25 @@ OPTIONS
|
|
|
22
22
|
-s, --seek <sec> Video start time for detection
|
|
23
23
|
-d, --duration <sec> Analyze this many seconds of video
|
|
24
24
|
-v, --verbose
|
|
25
|
-
-h, --help
|
|
26
25
|
|
|
27
26
|
SEE ALSO
|
|
28
27
|
mediasnacks framediff
|
|
29
|
-
|
|
28
|
+
`
|
|
30
29
|
|
|
31
30
|
|
|
32
31
|
export default async function main() {
|
|
33
|
-
await
|
|
34
|
-
|
|
35
|
-
const { values, files } = await parseOptions({
|
|
32
|
+
const { values, files } = await parseOptions(HELP, {
|
|
36
33
|
seek: { short: 's', type: 'string', },
|
|
37
34
|
duration: { short: 'd', type: 'string' },
|
|
38
|
-
help: { short: 'h', type: 'boolean' }
|
|
39
35
|
})
|
|
40
36
|
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (files.length !== 1) throw new Error('Invalid input file. One video file must be specified. See mediasnacks detectdups --help')
|
|
37
|
+
if (files.length !== 1)
|
|
38
|
+
throw 'Invalid input file. One video file must be specified.'
|
|
47
39
|
|
|
48
40
|
const video = files[0]
|
|
49
41
|
const v = await videoAttrs(video)
|
|
50
|
-
if (v.codec_type !== 'video')
|
|
42
|
+
if (v.codec_type !== 'video')
|
|
43
|
+
throw 'Invalid input file. Must be a video.'
|
|
51
44
|
|
|
52
45
|
const vDur = Number(v.duration)
|
|
53
46
|
|
|
@@ -59,9 +52,9 @@ export default async function main() {
|
|
|
59
52
|
? Number(values.duration)
|
|
60
53
|
: vDur > 60 ? 20 : vDur
|
|
61
54
|
|
|
62
|
-
if (isNaN(seek) || seek < 0) throw
|
|
63
|
-
if (isNaN(duration) || duration < 1) throw
|
|
64
|
-
if ((seek + duration) > vDur) throw
|
|
55
|
+
if (isNaN(seek) || seek < 0) throw `Invalid --seek value: ${values.seek}`
|
|
56
|
+
if (isNaN(duration) || duration < 1) throw `Invalid --duration value: ${values.duration}`
|
|
57
|
+
if ((seek + duration) > vDur) throw `Invalid analysis range. Exceeds video duration: ${vDur}`
|
|
65
58
|
|
|
66
59
|
const dups = await detectdups({ video: files[0], seek, duration })
|
|
67
60
|
const h = deltaHistogram(dups)
|
package/src/dlaudio.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { parseOptions } from './utils/parseOptions.js'
|
|
2
|
+
import { runSilently } from './utils/subprocess.js'
|
|
3
|
+
import { unemoji } from './unemoji.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const HELP = `
|
|
7
|
+
SYNOPSIS
|
|
8
|
+
mediasnacks dlaudio <url>
|
|
9
|
+
|
|
10
|
+
DESCRIPTION
|
|
11
|
+
yt-dlp best m4a
|
|
12
|
+
`
|
|
13
|
+
|
|
14
|
+
export default async function main() {
|
|
15
|
+
const { values, positionals } = await parseOptions(HELP)
|
|
16
|
+
|
|
17
|
+
if (!positionals[0])
|
|
18
|
+
throw 'Missing URL'
|
|
19
|
+
|
|
20
|
+
const f = await dlaudio(positionals[0])
|
|
21
|
+
console.log(f)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function dlaudio(url) {
|
|
25
|
+
const f = (await runSilently('yt-dlp', [
|
|
26
|
+
'--print', 'filename',
|
|
27
|
+
'--no-simulate',
|
|
28
|
+
'-o', '%(title)s.%(ext)s',
|
|
29
|
+
'-f', 'bestaudio[ext=m4a]/bestaudio',
|
|
30
|
+
url
|
|
31
|
+
])).stdout.trim()
|
|
32
|
+
|
|
33
|
+
return await unemoji(f) || f
|
|
34
|
+
}
|
package/src/dlvideo.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { parseOptions } from './utils/parseOptions.js'
|
|
2
|
+
import { run } from './utils/subprocess.js'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
const HELP = `
|
|
6
|
+
SYNOPSIS
|
|
7
|
+
mediasnacks dlvideo <url>
|
|
8
|
+
|
|
9
|
+
DESCRIPTION
|
|
10
|
+
yt-dlp best mp4
|
|
11
|
+
`
|
|
12
|
+
|
|
13
|
+
export default async function main() {
|
|
14
|
+
const { values, positionals } = await parseOptions(HELP)
|
|
15
|
+
|
|
16
|
+
if (!positionals[0])
|
|
17
|
+
throw 'Missing URL'
|
|
18
|
+
|
|
19
|
+
await dlvideo(positionals[0])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function dlvideo(url) {
|
|
23
|
+
await run('yt-dlp', [
|
|
24
|
+
'-o', '%(title)s.%(ext)s',
|
|
25
|
+
'-f', 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/mp4',
|
|
26
|
+
url
|
|
27
|
+
])
|
|
28
|
+
}
|
package/src/dropdups.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve, parse, format } from 'node:path'
|
|
2
2
|
import { parseOptions } from './utils/parseOptions.js'
|
|
3
|
-
import { ffmpeg,
|
|
3
|
+
import { ffmpeg, run } from './utils/subprocess.js'
|
|
4
4
|
import { ProresProfiles } from './prores.js'
|
|
5
5
|
|
|
6
6
|
|
|
@@ -16,7 +16,6 @@ DESCRIPTION
|
|
|
16
16
|
OPTIONS
|
|
17
17
|
-n, --dup-frame-num <n> Known frame interval to drop.
|
|
18
18
|
Default: n=0, which auto-detects repeated frames (slower)
|
|
19
|
-
-h, --help
|
|
20
19
|
|
|
21
20
|
EXAMPLES
|
|
22
21
|
Use n=2 when every other frame is repeated:
|
|
@@ -24,30 +23,21 @@ EXAMPLES
|
|
|
24
23
|
|
|
25
24
|
Use n=6 if e.g., a 25 fps got upped to 30 fps without interpolation.
|
|
26
25
|
mediasnacks dropdups -n6 vid.mov
|
|
27
|
-
|
|
26
|
+
`
|
|
28
27
|
|
|
29
28
|
|
|
30
29
|
export default async function main() {
|
|
31
|
-
await
|
|
32
|
-
|
|
33
|
-
const { values, files } = await parseOptions({
|
|
34
|
-
'dup-frame-num': { short: 'n', type: 'string', default: '' },
|
|
35
|
-
help: { short: 'h', type: 'boolean' },
|
|
30
|
+
const { values, files } = await parseOptions(HELP, {
|
|
31
|
+
'dup-frame-num': { short: 'n', type: 'string' },
|
|
36
32
|
})
|
|
37
33
|
|
|
38
|
-
if (values.help) {
|
|
39
|
-
console.log(HELP)
|
|
40
|
-
return
|
|
41
|
-
}
|
|
42
|
-
|
|
43
34
|
if (!files.length)
|
|
44
|
-
throw
|
|
35
|
+
throw 'No video specified.'
|
|
45
36
|
|
|
46
37
|
let dupFrameNum = values['dup-frame-num']
|
|
47
38
|
if (dupFrameNum && !Number.isInteger(+dupFrameNum))
|
|
48
|
-
throw
|
|
39
|
+
throw 'Invalid -n. It must be a positive integer.'
|
|
49
40
|
|
|
50
|
-
console.log('Dropping Duplicate Frames…')
|
|
51
41
|
for (const file of files)
|
|
52
42
|
await dropdups(resolve(file), dupFrameNum)
|
|
53
43
|
}
|
package/src/edgespic.js
CHANGED
|
@@ -3,46 +3,38 @@ import { basename, extname, join, parse } from 'node:path'
|
|
|
3
3
|
import { mkDir } from './utils/fs-utils.js'
|
|
4
4
|
import { videoAttrs } from './utils/videoAttrs.js'
|
|
5
5
|
import { parseOptions } from './utils/parseOptions.js'
|
|
6
|
-
import { ffmpeg
|
|
6
|
+
import { ffmpeg } from './utils/subprocess.js'
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
const WIDTH = 640
|
|
10
|
+
|
|
9
11
|
const HELP = `
|
|
10
12
|
SYNOPSIS
|
|
11
|
-
mediasnacks edgespic [--width=<num>] <files>
|
|
13
|
+
mediasnacks edgespic [-w | --width=<num>] <files>
|
|
12
14
|
|
|
13
15
|
DESCRIPTION
|
|
14
16
|
Extracts the first and last frames from each video and saves them to the 'edgepics/' subfolder.
|
|
15
17
|
|
|
16
18
|
OPTIONS
|
|
17
|
-
-w, --width Default:
|
|
19
|
+
-w, --width Default: ${WIDTH} The aspect ratio is preserved.
|
|
18
20
|
|
|
19
21
|
EXAMPLES
|
|
20
22
|
mediasnacks edgespic -w 800 *.mov
|
|
21
23
|
mediasnacks edgespic -w 600 'videos/**/*.mp4'
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
`
|
|
24
25
|
|
|
25
26
|
export default async function main() {
|
|
26
|
-
await
|
|
27
|
-
|
|
28
|
-
const { values, files } = await parseOptions({
|
|
29
|
-
'width': { short: 'w', type: 'string', default: '640' },
|
|
30
|
-
help: { short: 'h', type: 'boolean' },
|
|
27
|
+
const { values, files } = await parseOptions(HELP, {
|
|
28
|
+
width: { short: 'w', type: 'string', default: String(WIDTH) }
|
|
31
29
|
})
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const width = Number(values['width'])
|
|
39
|
-
if (width <= 0 || !Number.isInteger(width)) throw new Error('--width must be a positive number')
|
|
40
|
-
if (!files.length) throw new Error('No video files specified')
|
|
31
|
+
const width = Number(values.width)
|
|
32
|
+
if (width <= 0 || !Number.isInteger(width)) throw '--width must be a positive number'
|
|
33
|
+
if (!files.length) throw 'No video files specified'
|
|
41
34
|
|
|
42
35
|
const outDir = join(parse(files[0]).dir, 'edgespic')
|
|
43
36
|
await mkDir(outDir)
|
|
44
37
|
|
|
45
|
-
console.log('Extracting edge frames…')
|
|
46
38
|
for (const file of files)
|
|
47
39
|
await edgespic(file, width, outDir)
|
|
48
40
|
}
|
package/src/frameseq.js
CHANGED
|
@@ -2,7 +2,7 @@ import { basename, extname, join, parse } from 'node:path'
|
|
|
2
2
|
|
|
3
3
|
import { mkDir } from './utils/fs-utils.js'
|
|
4
4
|
import { parseOptions } from './utils/parseOptions.js'
|
|
5
|
-
import { ffmpeg
|
|
5
|
+
import { ffmpeg } from './utils/subprocess.js'
|
|
6
6
|
import { countframes } from './countframes.js'
|
|
7
7
|
|
|
8
8
|
|
|
@@ -24,31 +24,23 @@ EXAMPLES
|
|
|
24
24
|
|
|
25
25
|
Custom framerate, all video duration
|
|
26
26
|
mediasnacks frameseq --fps=12 video.mov
|
|
27
|
-
|
|
27
|
+
`
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
export default async function main() {
|
|
31
|
-
await
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
start: { short: 's', type: 'string', default: '' },
|
|
36
|
-
end: { short: 'e', type: 'string', default: '' },
|
|
31
|
+
const { values, files } = await parseOptions(HELP, {
|
|
32
|
+
fps: { short: 'f', type: 'string' },
|
|
33
|
+
start: { short: 's', type: 'string' },
|
|
34
|
+
end: { short: 'e', type: 'string' },
|
|
37
35
|
outdir: { type: 'string', default: '' },
|
|
38
|
-
help: { short: 'h', type: 'boolean' }
|
|
39
36
|
})
|
|
40
37
|
|
|
41
|
-
if (values.help) {
|
|
42
|
-
console.log(HELP)
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
|
|
46
38
|
const { fps, start, end, outdir } = values
|
|
47
39
|
const video = files[0]
|
|
48
|
-
if (!video) throw
|
|
49
|
-
if (fps && isNaN(parseFloat(fps))) throw
|
|
50
|
-
if (start && isNaN(parseFloat(start))) throw
|
|
51
|
-
if (end && isNaN(parseFloat(end))) throw
|
|
40
|
+
if (!video) throw 'No video files specified'
|
|
41
|
+
if (fps && isNaN(parseFloat(fps))) throw 'Invalid --fps'
|
|
42
|
+
if (start && isNaN(parseFloat(start))) throw 'Invalid --start'
|
|
43
|
+
if (end && isNaN(parseFloat(end))) throw 'Invalid --end'
|
|
52
44
|
|
|
53
45
|
const nFrames = await countframes({ video, fps, start, end })
|
|
54
46
|
const pad = String(nFrames).length
|
|
@@ -57,13 +49,13 @@ export default async function main() {
|
|
|
57
49
|
|
|
58
50
|
export async function frameseq({ video, fps, start, end, pad, outdir }) {
|
|
59
51
|
const name = basename(video, extname(video))
|
|
60
|
-
const
|
|
61
|
-
await mkDir(
|
|
52
|
+
const dir = outdir || join(parse(video).dir, name)
|
|
53
|
+
await mkDir(dir)
|
|
62
54
|
await ffmpeg([
|
|
63
55
|
start ? ['-ss', start] : [],
|
|
64
56
|
end ? ['-to', end] : [],
|
|
65
57
|
'-i', video,
|
|
66
58
|
fps ? ['-vf', `fps=${fps}`] : [],
|
|
67
|
-
join(
|
|
59
|
+
join(dir, `${name}_%0${pad}d.png`)
|
|
68
60
|
].flat())
|
|
69
61
|
}
|
package/src/gif.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { parse, format } from 'node:path'
|
|
2
|
+
import { parseOptions } from './utils/parseOptions.js'
|
|
3
|
+
import { run } from './utils/subprocess.js'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const FPS = 12
|
|
7
|
+
const WIDTH = 600
|
|
8
|
+
|
|
9
|
+
const HELP = `
|
|
10
|
+
SYNOPSIS
|
|
11
|
+
mediasnacks gif [-f | --fps <number>] [-w | --width <pixels>] <file>
|
|
12
|
+
|
|
13
|
+
DESCRIPTION
|
|
14
|
+
Converts video to GIF
|
|
15
|
+
|
|
16
|
+
OPTIONS
|
|
17
|
+
-f, --fps Default: ${FPS}
|
|
18
|
+
-w, --width Default: ${WIDTH}
|
|
19
|
+
`
|
|
20
|
+
|
|
21
|
+
export default async function main() {
|
|
22
|
+
const { values, files } = await parseOptions(HELP, {
|
|
23
|
+
fps: { short: 'f', type: 'string', default: String(FPS) },
|
|
24
|
+
width: { short: 'w', type: 'string', default: String(WIDTH) },
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (!files.length)
|
|
28
|
+
throw 'Missing input file'
|
|
29
|
+
|
|
30
|
+
await gif(files[0], values.fps, values.width)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function gif(file, fps, width) {
|
|
34
|
+
const { dir, name } = parse(file)
|
|
35
|
+
const outName = format({ dir, name, ext: '.gif' })
|
|
36
|
+
|
|
37
|
+
await run('ffmpeg', [
|
|
38
|
+
'-v', 'error',
|
|
39
|
+
'-i', file,
|
|
40
|
+
'-vf', `fps=${fps},scale=${width}:-1`,
|
|
41
|
+
outName,
|
|
42
|
+
])
|
|
43
|
+
}
|
package/src/hev1tohvc1.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseOptions } from './utils/parseOptions.js'
|
|
2
2
|
import { uniqueFilenameFor, overwrite } from './utils/fs-utils.js'
|
|
3
|
-
import { ffmpeg
|
|
3
|
+
import { ffmpeg } from './utils/subprocess.js'
|
|
4
4
|
import { videoAttrs } from './utils/videoAttrs.js'
|
|
5
5
|
|
|
6
6
|
|
|
@@ -12,36 +12,25 @@ DESCRIPTION
|
|
|
12
12
|
This program fixes video thumbnails not rendering in macOS
|
|
13
13
|
Finder, and fixes video not importable in Final Cut Pro. That’s done
|
|
14
14
|
by changing the container’s sample entry code from HEV1 to HVC1.
|
|
15
|
-
|
|
15
|
+
`
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
export default async function main() {
|
|
19
|
-
await
|
|
19
|
+
const { values, files } = await parseOptions(HELP)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
})
|
|
21
|
+
if (!files.length)
|
|
22
|
+
throw 'Missing input file(s)'
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
await hev1tohvc1(file)
|
|
26
|
+
console.log(file)
|
|
28
27
|
}
|
|
29
|
-
|
|
30
|
-
if (!files.length) throw new Error('Missing input file(s)')
|
|
31
|
-
|
|
32
|
-
for (const file of files)
|
|
33
|
-
try {
|
|
34
|
-
await hev1tohvc1(file)
|
|
35
|
-
console.log(file)
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
console.error(err?.message || err)
|
|
39
|
-
}
|
|
40
28
|
}
|
|
41
29
|
|
|
42
30
|
export async function hev1tohvc1(file) {
|
|
43
31
|
const v = await videoAttrs(file)
|
|
44
|
-
if (v.codec_tag_string !== 'hev1')
|
|
32
|
+
if (v.codec_tag_string !== 'hev1')
|
|
33
|
+
throw `non hev1 ${file}`
|
|
45
34
|
|
|
46
35
|
const tmp = uniqueFilenameFor(file)
|
|
47
36
|
await ffmpeg([
|