mediasnacks 0.27.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/package.json +1 -1
- package/src/avif.js +17 -29
- package/src/cli.js +14 -17
- package/src/countframes.js +10 -19
- package/src/detectdups.js +10 -17
- package/src/dlaudio.js +5 -9
- package/src/dlvideo.js +4 -8
- 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 +5 -9
- 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 +20 -30
- 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/gif.sh +0 -44
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
CHANGED
|
@@ -11,19 +11,15 @@ DESCRIPTION
|
|
|
11
11
|
|
|
12
12
|
EXAMPLE
|
|
13
13
|
mediasnacks png *.png
|
|
14
|
-
|
|
14
|
+
`
|
|
15
15
|
|
|
16
16
|
export default async function main() {
|
|
17
|
-
const { values,
|
|
18
|
-
help: { short: 'h', type: 'boolean' }
|
|
19
|
-
})
|
|
17
|
+
const { values, files } = await parseOptions(HELP)
|
|
20
18
|
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
return
|
|
24
|
-
}
|
|
19
|
+
if (!files.length)
|
|
20
|
+
throw 'Missing input image(s)'
|
|
25
21
|
|
|
26
|
-
await png(...
|
|
22
|
+
await png(...files)
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
export async function png(...images) {
|
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
CHANGED
|
@@ -4,6 +4,16 @@ import { dirname, basename, join } from 'node:path'
|
|
|
4
4
|
import { parseOptions } from './utils/parseOptions.js'
|
|
5
5
|
import { findFiles } from './utils/fs-utils.js'
|
|
6
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
|
+
|
|
7
17
|
const EMOJI_RE = new RegExp(
|
|
8
18
|
'[' +
|
|
9
19
|
'\u{1F600}-\u{1F64F}' + // Emoticons
|
|
@@ -18,30 +28,13 @@ const EMOJI_RE = new RegExp(
|
|
|
18
28
|
'gu'
|
|
19
29
|
)
|
|
20
30
|
|
|
21
|
-
const HELP = `
|
|
22
|
-
SYNOPSIS
|
|
23
|
-
mediasnacks unemoji [-r | --recursive] <dir>
|
|
24
|
-
|
|
25
|
-
DESCRIPTION
|
|
26
|
-
Removes emoji from filenames in the current directory.
|
|
27
|
-
Does not overwrite files.
|
|
28
|
-
|
|
29
|
-
OPTIONS
|
|
30
|
-
-r, --recursive
|
|
31
|
-
`.trim()
|
|
32
|
-
|
|
33
31
|
export default async function main() {
|
|
34
|
-
const { values, positionals } = await parseOptions({
|
|
35
|
-
help: { short: 'h', type: 'boolean' },
|
|
32
|
+
const { values, positionals } = await parseOptions(HELP, {
|
|
36
33
|
recursive: { short: 'r', type: 'boolean' }
|
|
37
34
|
})
|
|
38
35
|
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (positionals.length !== 1) throw new Error('Only one dir is accepted')
|
|
36
|
+
if (positionals.length !== 1)
|
|
37
|
+
throw 'Must pass only one dir'
|
|
45
38
|
|
|
46
39
|
const files = findFiles({
|
|
47
40
|
dir: positionals[0],
|
|
@@ -49,15 +42,11 @@ export default async function main() {
|
|
|
49
42
|
recursive: values.recursive,
|
|
50
43
|
})
|
|
51
44
|
|
|
52
|
-
for (const file of files)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
catch (err) {
|
|
59
|
-
console.error(err?.message || err)
|
|
60
|
-
}
|
|
45
|
+
for (const file of files) {
|
|
46
|
+
const newpath = await unemoji(file)
|
|
47
|
+
if (newpath)
|
|
48
|
+
console.log(`Renaming: ${file} -> ${newpath}`)
|
|
49
|
+
}
|
|
61
50
|
}
|
|
62
51
|
|
|
63
52
|
export async function unemoji(file) {
|
|
@@ -72,7 +61,8 @@ export async function unemoji(file) {
|
|
|
72
61
|
return null
|
|
73
62
|
|
|
74
63
|
const newpath = join(dir, newbase)
|
|
75
|
-
if (existsSync(newpath))
|
|
64
|
+
if (existsSync(newpath))
|
|
65
|
+
throw `Skipping (exists): ${file} -> ${newpath}`
|
|
76
66
|
|
|
77
67
|
await rename(file, newpath)
|
|
78
68
|
return newpath
|
|
@@ -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
|
}
|
package/src/vsplit.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs'
|
|
|
2
2
|
import { resolve, parse, join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
import { parseOptions } from './utils/parseOptions.js'
|
|
5
|
-
import {
|
|
5
|
+
import { run } from './utils/subprocess.js'
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
// TODO looks like it's missing a frame (perhaps becaue of -c copy)
|
|
@@ -31,29 +31,20 @@ EXAMPLE
|
|
|
31
31
|
|
|
32
32
|
SEE ALSO
|
|
33
33
|
mediasnacks vtrim
|
|
34
|
-
|
|
34
|
+
`
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
export default async function main() {
|
|
38
|
-
await
|
|
39
|
-
|
|
40
|
-
const { values, files } = await parseOptions({
|
|
41
|
-
help: { short: 'h', type: 'boolean' },
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
if (values.help) {
|
|
45
|
-
console.log(HELP)
|
|
46
|
-
return
|
|
47
|
-
}
|
|
38
|
+
const { values, files } = await parseOptions(HELP)
|
|
48
39
|
|
|
49
40
|
if (files.length !== 2)
|
|
50
|
-
throw
|
|
41
|
+
throw 'Expected 2 arguments: CSV file and video file.'
|
|
51
42
|
|
|
52
43
|
const [csvPath, videoPath] = files.map(f => resolve(f))
|
|
53
44
|
|
|
54
45
|
const clips = parseCSV(csvPath)
|
|
55
46
|
if (!clips.length)
|
|
56
|
-
throw
|
|
47
|
+
throw 'CSV file contains no clips'
|
|
57
48
|
|
|
58
49
|
console.log(`Splitting video into ${clips.length} clip${clips.length === 1 ? '' : 's'}…`)
|
|
59
50
|
await vsplit(videoPath, clips)
|
|
@@ -64,14 +55,14 @@ function parseCSV(csvPath) {
|
|
|
64
55
|
const lines = content.split('\n').filter(line => line.trim())
|
|
65
56
|
|
|
66
57
|
if (!lines.length)
|
|
67
|
-
throw
|
|
58
|
+
throw 'CSV file is empty'
|
|
68
59
|
|
|
69
60
|
const clips = []
|
|
70
61
|
for (let i = 1; i < lines.length; i++) { // unconditionally skips header
|
|
71
62
|
const parts = lines[i].split(',').map(s => s.trim())
|
|
72
63
|
|
|
73
64
|
if (parts.length !== 2)
|
|
74
|
-
throw
|
|
65
|
+
throw `Invalid CSV format at line ${i + 1}: expected 2 columns, got ${parts.length}`
|
|
75
66
|
|
|
76
67
|
clips.push(parts)
|
|
77
68
|
}
|
|
@@ -80,7 +71,7 @@ function parseCSV(csvPath) {
|
|
|
80
71
|
|
|
81
72
|
export async function vsplit(videoPath, clips) {
|
|
82
73
|
const { dir, name, ext } = parse(videoPath)
|
|
83
|
-
const seqLen =
|
|
74
|
+
const seqLen = String(clips.length).length
|
|
84
75
|
|
|
85
76
|
for (let i = 0; i < clips.length; i++) {
|
|
86
77
|
const [start, end] = clips[i]
|
package/src/vtrim.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { resolve, parse } from 'node:path'
|
|
2
2
|
import { parseOptions } from './utils/parseOptions.js'
|
|
3
|
-
import { ffmpeg
|
|
3
|
+
import { ffmpeg } from './utils/subprocess.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
const HELP = `
|
|
7
7
|
SYNOPSIS
|
|
8
|
-
mediasnacks vtrim [
|
|
8
|
+
mediasnacks vtrim [options] <videos>
|
|
9
9
|
|
|
10
10
|
DESCRIPTION
|
|
11
11
|
Trims a video without re-encoding (fast, but approximate cuts).
|
|
@@ -16,25 +16,17 @@ OPTIONS
|
|
|
16
16
|
|
|
17
17
|
SEE ALSO
|
|
18
18
|
mediasnacks vsplit
|
|
19
|
-
|
|
19
|
+
`
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
export default async function main() {
|
|
23
|
-
await
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
start: { short: 's', type: 'string', default: '' },
|
|
27
|
-
end: { short: 'e', type: 'string', default: '' },
|
|
28
|
-
help: { short: 'h', type: 'boolean' }
|
|
23
|
+
const { values, files } = await parseOptions(HELP, {
|
|
24
|
+
start: { short: 's', type: 'string' },
|
|
25
|
+
end: { short: 'e', type: 'string' },
|
|
29
26
|
})
|
|
30
27
|
|
|
31
|
-
if (values.help) {
|
|
32
|
-
console.log(HELP)
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
28
|
if (!files.length)
|
|
37
|
-
throw
|
|
29
|
+
throw 'No video specified.'
|
|
38
30
|
|
|
39
31
|
for (const file of files)
|
|
40
32
|
await vtrim({
|