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 +3 -54
- package/install-zsh-completions.js +48 -0
- package/package.json +3 -4
- package/src/avif.js +5 -5
- package/src/cli.js +38 -28
- package/src/detectdups.js +28 -25
- package/src/detectdups.test.js +7 -19
- package/src/dropdups.js +6 -14
- package/src/edgespic.js +5 -5
- package/src/hev1tohvc1.js +5 -5
- package/src/moov2front.js +7 -3
- package/src/play.js +64 -0
- package/src/prores.js +19 -15
- package/src/qdir.js +2 -2
- package/src/random.js +45 -0
- package/src/resize.js +3 -3
- package/src/seqcheck.js +2 -2
- package/src/sqcrop.js +3 -3
- package/src/ssim.js +3 -3
- package/src/utils/parseOptions.js +1 -0
- package/src/utils/parseOptions.test.js +2 -1
- package/src/vconcat.test.js +1 -1
- package/src/vsplit.js +5 -5
- package/src/vsplit.test.js +1 -1
- package/src/vtrim.js +5 -5
- package/src/vtrim.test.js +1 -1
- package/.zsh/completions/_mediasnacks +0 -49
- package/install-zsh-completions.sh +0 -23
- package/src/curltime.sh +0 -14
- /package/src/utils/{ffmpeg.js → subprocess.js} +0 -0
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.
|
|
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.
|
|
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": "
|
|
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
|
-
".
|
|
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/
|
|
7
|
+
import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
const
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
39
|
+
rmcover: ['rmcover.sh', 'Removes cover art'],
|
|
40
|
+
}
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
export function commandsSummary() {
|
|
43
|
+
return Object.entries(COMMANDS)
|
|
44
|
+
.map(([cmd, [, desc]]) => [cmd, desc])
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
const
|
|
47
|
+
const HELP = `
|
|
43
48
|
SYNOPSIS
|
|
44
49
|
mediasnacks <command> <args>
|
|
45
50
|
|
|
46
51
|
COMMANDS
|
|
47
|
-
${
|
|
48
|
-
` ${styleText('bold', cmd.padEnd(12, ' '))}\t${
|
|
52
|
+
${commandsSummary().map(([cmd, desc]) =>
|
|
53
|
+
` ${styleText('bold', cmd.padEnd(12, ' '))}\t${desc}`).join('\n')}
|
|
49
54
|
`.trim()
|
|
50
55
|
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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/
|
|
4
|
+
import { ffmpeg, assertUserHasFFmpeg, videoAttrs } from './utils/subprocess.js'
|
|
5
5
|
|
|
6
6
|
const STDEV_THRESHOLD = 0.2
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const HELP = `
|
|
9
|
+
SYNOPSIS
|
|
10
|
+
mediasnacks detectdups [options] <video>
|
|
10
11
|
|
|
11
|
-
|
|
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(
|
|
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
|
|
74
|
-
|
|
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
|
|
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
|
|
120
|
+
function deltaHistogram(dups) {
|
|
110
121
|
const histogram = {}
|
|
111
|
-
for (let i = 1; i <
|
|
112
|
-
const diff =
|
|
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
|
-
|
|
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(
|
|
129
|
+
function maxFreqKey(histogram) {
|
|
126
130
|
let maxKey = null
|
|
127
131
|
let maxVal = -1
|
|
128
|
-
for (const [
|
|
129
|
-
if (
|
|
130
|
-
maxVal =
|
|
131
|
-
maxKey =
|
|
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)
|
package/src/detectdups.test.js
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
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/
|
|
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(
|
|
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
|
|
50
|
+
await dropdups(resolve(file), nBadFrame)
|
|
59
51
|
}
|
|
60
52
|
|
|
61
|
-
async function
|
|
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/
|
|
7
|
+
import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/subprocess.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
const
|
|
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(
|
|
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
|
|
49
|
+
await edgespic(file, width, outDir)
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
|
|
53
|
-
async function
|
|
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/
|
|
5
|
+
import { videoAttrs, ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
const
|
|
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(
|
|
25
|
+
throw new Error(HELP)
|
|
26
26
|
|
|
27
27
|
console.log('HEV1 to HVC1…')
|
|
28
28
|
for (const file of files)
|
|
29
|
-
await
|
|
29
|
+
await hev1tohvc1(file)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
async function
|
|
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/
|
|
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
|
|
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(
|
|
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/
|
|
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
|
|
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(
|
|
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
|
|
53
|
-
|
|
54
|
-
const
|
|
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',
|
|
64
|
+
'-i', video,
|
|
62
65
|
'-c:v', 'prores_ks',
|
|
63
|
-
'-profile:v',
|
|
66
|
+
'-profile:v', profile,
|
|
64
67
|
'-pix_fmt', 'yuv422p10le',
|
|
65
|
-
|
|
68
|
+
output
|
|
66
69
|
])
|
|
67
70
|
}
|
|
68
71
|
|
|
69
|
-
main
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
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(
|
|
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/
|
|
8
|
+
import { ffmpeg, videoAttrs, assertUserHasFFmpeg } from './utils/subprocess.js'
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
const
|
|
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(
|
|
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
|
|
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(
|
|
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/
|
|
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
|
|
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(
|
|
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/
|
|
3
|
+
import { ffmpeg } from './utils/subprocess.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
const
|
|
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(
|
|
18
|
+
console.log(HELP)
|
|
19
19
|
process.exit(1)
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -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')
|
package/src/vconcat.test.js
CHANGED
|
@@ -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/
|
|
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/
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
package/src/vsplit.test.js
CHANGED
|
@@ -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/
|
|
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/
|
|
5
|
+
import { ffmpeg, assertUserHasFFmpeg } from './utils/subprocess.js'
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
const
|
|
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(
|
|
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
|
|
40
|
+
await vtrim(resolve(file), values.start, values.end)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
async function
|
|
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/
|
|
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
|