mediasnacks 0.21.0 → 0.22.2
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 -52
- package/install-zsh-completions.js +48 -0
- package/package.json +3 -4
- package/src/avif.js +5 -5
- package/src/cli.js +36 -24
- package/src/detectdups.js +9 -7
- 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/vsplit.js +5 -5
- package/src/vtrim.js +5 -5
- package/.zsh/completions/_mediasnacks +0 -47
- package/install-zsh-completions.sh +0 -23
- package/src/avif.test.js +0 -16
- package/src/detectdups.test.js +0 -24
- package/src/edgespic.test.js +0 -36
- package/src/fixtures/60fps.csv +0 -7
- package/src/fixtures/60fps.mp4 +0 -0
- package/src/fixtures/big-buck-bunny/bbb_24_to_25fps_dup.mp4 +0 -0
- package/src/fixtures/big-buck-bunny/bbb_24_to_30fps_dup.mp4 +0 -0
- package/src/fixtures/big-buck-bunny/bbb_24_to_48fps_dup.mp4 +0 -0
- package/src/fixtures/big-buck-bunny/bbb_24fps_no_dups.mp4 +0 -0
- package/src/fixtures/big-buck-bunny/bbb_25_to_30fps_dup.mp4 +0 -0
- package/src/fixtures/big-buck-bunny/bbb_25_to_50fps_dup.mp4 +0 -0
- package/src/fixtures/big-buck-bunny/bbb_25_to_60fps_dup.mp4 +0 -0
- package/src/fixtures/big-buck-bunny/bbb_25fps_no_dups.mp4 +0 -0
- package/src/fixtures/big-buck-bunny/generate.md +0 -71
- package/src/fixtures/edgespic/60fps_first.png +0 -0
- package/src/fixtures/edgespic/60fps_last.png +0 -0
- package/src/fixtures/lenna.avif +0 -0
- package/src/fixtures/lenna.png +0 -0
- package/src/fixtures/qdir-jobs/job1_good.sh +0 -1
- package/src/fixtures/qdir-jobs/job2_bad.sh +0 -1
- package/src/fixtures/qdir-jobs/job3_good.sh +0 -2
- package/src/fixtures/qdir-jobs/job4_bad.sh +0 -1
- package/src/flattendir.test.js +0 -36
- package/src/qdir.test.js +0 -24
- package/src/seqcheck.test.js +0 -27
- package/src/utils/fs-utils.test.js +0 -21
- package/src/utils/parseOptions.test.js +0 -59
- package/src/vconcat.test.js +0 -23
- package/src/vsplit.test.js +0 -32
- package/src/vtrim.test.js +0 -40
- /package/src/utils/{ffmpeg.js → subprocess.js} +0 -0
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
|
|
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/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',
|
|
@@ -1,47 +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
|
-
|
|
34
|
-
if (( CURRENT == 2 )); then
|
|
35
|
-
_describe -t commands 'mediasnacks commands' _mediasnacks_commands
|
|
36
|
-
return
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
local cmd="$words[2]"
|
|
40
|
-
case "$cmd" in
|
|
41
|
-
avif|resize|sqcrop|moov2front|detectdups|dropdups|edgespic|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|vsplit|vtrim|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|flattendir|prores|ssim)
|
|
42
|
-
_files
|
|
43
|
-
;;
|
|
44
|
-
qdir)
|
|
45
|
-
_files -/
|
|
46
|
-
;;
|
|
47
|
-
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/avif.test.js
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
|
-
import { test } from 'node:test'
|
|
3
|
-
import { ok } from 'node:assert/strict'
|
|
4
|
-
|
|
5
|
-
import { ssim } from './ssim.js'
|
|
6
|
-
import { mkTempDir, cli } from './utils/test-utils.js'
|
|
7
|
-
|
|
8
|
-
const rel = f => join(import.meta.dirname, f)
|
|
9
|
-
|
|
10
|
-
test('PNG to AVIF', async () => {
|
|
11
|
-
const tmp = mkTempDir('avif')
|
|
12
|
-
cli('avif', '--output-dir', tmp, rel('fixtures/lenna.png'))
|
|
13
|
-
|
|
14
|
-
const similarityScore = await ssim(join(tmp, 'lenna.avif'), rel('fixtures/lenna.avif'))
|
|
15
|
-
ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
|
|
16
|
-
})
|
package/src/detectdups.test.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { equal } from 'node:assert/strict'
|
|
4
|
-
import { cli } from './utils/test-utils.js'
|
|
5
|
-
|
|
6
|
-
const rel = f => join(import.meta.dirname, f)
|
|
7
|
-
|
|
8
|
-
function detect(video) {
|
|
9
|
-
const { stdout } = cli('detectdups', rel(video))
|
|
10
|
-
return JSON.parse(stdout).n
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
test('no dups', () => equal(detect('fixtures/big-buck-bunny/bbb_24fps_no_dups.mp4'), null))
|
|
14
|
-
|
|
15
|
-
// These fixtures are badly retimed (non-interpolated, just duplicating a frame)
|
|
16
|
-
|
|
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))
|
|
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))
|
|
22
|
-
|
|
23
|
-
test('24 to 25 (has dup at n=25)', () => equal(detect('fixtures/big-buck-bunny/bbb_24_to_25fps_dup.mp4'), 25))
|
|
24
|
-
|
package/src/edgespic.test.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { ok } from 'node:assert/strict'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { describe, test } from 'node:test'
|
|
4
|
-
import { cpSync, readdirSync, } from 'node:fs'
|
|
5
|
-
|
|
6
|
-
import { ssim } from './ssim.js'
|
|
7
|
-
import { cli, mkTempDir } from './utils/test-utils.js'
|
|
8
|
-
|
|
9
|
-
const rel = f => join(import.meta.dirname, f)
|
|
10
|
-
|
|
11
|
-
describe('edgespic', () => {
|
|
12
|
-
const tmp = mkTempDir('edgespic')
|
|
13
|
-
const inputFile = join(tmp, '60fps.mp4')
|
|
14
|
-
cpSync(rel('fixtures/60fps.mp4'), inputFile)
|
|
15
|
-
cli('edgespic', inputFile)
|
|
16
|
-
|
|
17
|
-
test('creates output directory', () => {
|
|
18
|
-
const files = readdirSync(join(tmp, 'edgespic'))
|
|
19
|
-
ok(files.length === 2, `Expected 2 PNG files, got ${files.length}`)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('extracts first frame', async () => {
|
|
23
|
-
const out = join(tmp, 'edgespic', '60fps_first.png')
|
|
24
|
-
const fixture = rel('fixtures/edgespic/60fps_first.png')
|
|
25
|
-
const similarityScore = await ssim(out, fixture)
|
|
26
|
-
ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
test('extracts last frame', async () => {
|
|
30
|
-
const out = join(tmp, 'edgespic', '60fps_last.png')
|
|
31
|
-
const fixture = rel('fixtures/edgespic/60fps_last.png')
|
|
32
|
-
const similarityScore = await ssim(out, fixture)
|
|
33
|
-
ok(similarityScore > 0.99, `Similarity too low: ${similarityScore}`)
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
|
package/src/fixtures/60fps.csv
DELETED
package/src/fixtures/60fps.mp4
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
# Generating Fixtures
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
## 1. Download a video (Big Buck Bunny)
|
|
5
|
-
|
|
6
|
-
```sh
|
|
7
|
-
curl https://download.blender.org/peach/bigbuckbunny_movies/big_buck_bunny_1080p_h264.mov -o bbb_full_24fps.mov
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
## 2. Extract a scene
|
|
11
|
-
Using the third scene because it's complex enough to cover many edge cases.
|
|
12
|
-
It has a bird flapping its wings with motion blur. Also, animated text titles,
|
|
13
|
-
and fairly static frames after the title ends.
|
|
14
|
-
|
|
15
|
-
The scene is 1080p, 24fps, 7.28sec, h.264.
|
|
16
|
-
```sh
|
|
17
|
-
brew tap ericfortis/fcpscene
|
|
18
|
-
brew install fcpscene
|
|
19
|
-
fcpscene -m files bbb_full_24fps.mov
|
|
20
|
-
|
|
21
|
-
cp bbb/bbb_full_24fps_003.mov ./bbb_24fps_no_dups.mov
|
|
22
|
-
rm -rf bbb
|
|
23
|
-
rm bbb_full_24fps.mov
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
## 3. Re-encode the scene 24fps
|
|
27
|
-
This way all videos will share the same encoding, and no audio.
|
|
28
|
-
|
|
29
|
-
```sh
|
|
30
|
-
ffmpeg -i bbb_24fps_no_dups.mov \
|
|
31
|
-
-c:v libx264 -crf 18 -preset slow \
|
|
32
|
-
-an \
|
|
33
|
-
bbb_24fps_no_dups.mp4
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## 4. Retime (no dups) speed stretch to 25fps
|
|
37
|
-
For a good (no dups) 25fps, retiming by speeding it up.
|
|
38
|
-
```sh
|
|
39
|
-
ffmpeg -i bbb_24fps_no_dups.mp4 \
|
|
40
|
-
-vf "setpts=24/25*PTS" \
|
|
41
|
-
-r 25 \
|
|
42
|
-
bbb_25fps_no_dups.mp4
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
## 5. Retime by inserting duplicates (no interpolation)
|
|
47
|
-
```sh
|
|
48
|
-
for TARGET_FPS in 48 30 25; do
|
|
49
|
-
ffmpeg -i bbb_24fps_no_dups.mp4 \
|
|
50
|
-
-vf fps=$TARGET_FPS \
|
|
51
|
-
-c:v libx264 -crf 18 -preset slow \
|
|
52
|
-
-an \
|
|
53
|
-
"bbb_24_to_${TARGET_FPS}fps_dup.mp4"
|
|
54
|
-
done
|
|
55
|
-
|
|
56
|
-
for TARGET_FPS in 60 50 30; do
|
|
57
|
-
ffmpeg -i bbb_25fps_no_dups.mp4 \
|
|
58
|
-
-vf fps=$TARGET_FPS \
|
|
59
|
-
-c:v libx264 -crf 18 -preset slow \
|
|
60
|
-
-an \
|
|
61
|
-
"bbb_25_to_${TARGET_FPS}fps_dup.mp4"
|
|
62
|
-
done
|
|
63
|
-
```
|
|
64
|
-
Counting the cycle from 1 (not from 0):
|
|
65
|
-
|
|
66
|
-
- 24 to 48 (inserts dup at n=2) 0101
|
|
67
|
-
- 25 to 50 (inserts dup at n=2) 0101
|
|
68
|
-
- 24 to 30 (inserts dup at n=5) 0000100001
|
|
69
|
-
- 25 to 30 (inserts dup at n=6) 000001000001
|
|
70
|
-
- 24 to 25 (inserts dup at n=25) (0*24)1
|
|
71
|
-
- 25 to 60 (inserts dup at n=2 and n=3) 01011
|
|
Binary file
|
|
Binary file
|
package/src/fixtures/lenna.avif
DELETED
|
Binary file
|
package/src/fixtures/lenna.png
DELETED
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
exit 0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
exit 1
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
exit 1
|
package/src/flattendir.test.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import { deepEqual } from 'node:assert/strict'
|
|
3
|
-
import { readdirSync } from 'node:fs'
|
|
4
|
-
|
|
5
|
-
import { mkTempDir, cli, dir, touch } from './utils/test-utils.js'
|
|
6
|
-
|
|
7
|
-
test('flattendir moves files to top level and deletes empty dirs', () => {
|
|
8
|
-
const tmp = mkTempDir('flattendir')
|
|
9
|
-
dir(tmp, 'dir1', 'dir1-1')
|
|
10
|
-
dir(tmp, 'dir2')
|
|
11
|
-
touch(tmp, 'file1.txt')
|
|
12
|
-
touch(tmp, 'dir1', 'file2.txt')
|
|
13
|
-
touch(tmp, 'dir1', 'dir1-1', 'file3.txt')
|
|
14
|
-
touch(tmp, 'dir1', '.DS_Store')
|
|
15
|
-
|
|
16
|
-
cli('flattendir', tmp)
|
|
17
|
-
deepEqual(readdirSync(tmp).sort(), [
|
|
18
|
-
'file1.txt',
|
|
19
|
-
'file2.txt',
|
|
20
|
-
'file3.txt'
|
|
21
|
-
])
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('flattendir does not move files if filename collision occurs', () => {
|
|
25
|
-
const tmp = mkTempDir('flattendir-collision')
|
|
26
|
-
dir(tmp, 'dir1')
|
|
27
|
-
touch(tmp, 'file1.txt')
|
|
28
|
-
touch(tmp, 'dir1', 'file1.txt')
|
|
29
|
-
|
|
30
|
-
cli('flattendir', tmp)
|
|
31
|
-
deepEqual(readdirSync(tmp, { recursive: true }).sort(), [
|
|
32
|
-
'dir1',
|
|
33
|
-
'dir1/file1.txt',
|
|
34
|
-
'file1.txt'
|
|
35
|
-
])
|
|
36
|
-
})
|
package/src/qdir.test.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { equal, deepEqual } from 'node:assert/strict'
|
|
4
|
-
import { cpSync, readdirSync } from 'node:fs'
|
|
5
|
-
|
|
6
|
-
import { qdir } from './qdir.js'
|
|
7
|
-
import { mkTempDir } from './utils/test-utils.js'
|
|
8
|
-
|
|
9
|
-
const rel = f => join(import.meta.dirname, f)
|
|
10
|
-
|
|
11
|
-
test('qdir-jobs get renamed and failed have their exit status code', async () => {
|
|
12
|
-
const tmp = mkTempDir('qdir')
|
|
13
|
-
cpSync(rel('fixtures/qdir-jobs'), tmp, { recursive: true })
|
|
14
|
-
|
|
15
|
-
const err = await qdir(tmp, 0.2)
|
|
16
|
-
equal(err, null)
|
|
17
|
-
|
|
18
|
-
deepEqual(readdirSync(tmp).sort(), [
|
|
19
|
-
'job1_good.sh.done',
|
|
20
|
-
'job2_bad.sh.failed.1',
|
|
21
|
-
'job3_good.sh.done',
|
|
22
|
-
'job4_bad.sh.failed.1'
|
|
23
|
-
])
|
|
24
|
-
})
|
package/src/seqcheck.test.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test'
|
|
2
|
-
import { deepEqual } from 'node:assert/strict'
|
|
3
|
-
import { extractSeqNums, findMissingNumbers } from './seqcheck.js'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
test('extractSeqNums extracts sequence numbers from filenames', () => {
|
|
7
|
-
const filenames = [
|
|
8
|
-
'video-111_001.mov',
|
|
9
|
-
'video-111_002.mov',
|
|
10
|
-
'video-111_004.mov',
|
|
11
|
-
'bad.mov',
|
|
12
|
-
'bad_too_a39.mov',
|
|
13
|
-
]
|
|
14
|
-
deepEqual(extractSeqNums(filenames, '_', '.'), [1, 2, 4])
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
test('findMissingNumbers ', () => {
|
|
19
|
-
test('finds gaps in a sequence', () =>
|
|
20
|
-
deepEqual(findMissingNumbers([1, 2, 4, 5, 8]), [3, 6, 7]))
|
|
21
|
-
|
|
22
|
-
test('returns empty array for empty input', () =>
|
|
23
|
-
deepEqual(findMissingNumbers([]), []))
|
|
24
|
-
|
|
25
|
-
test('returns empty array when there are no gaps', () =>
|
|
26
|
-
deepEqual(findMissingNumbers([10, 11, 12]), []))
|
|
27
|
-
})
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { equal } from 'node:assert/strict'
|
|
2
|
-
import test, { describe } from 'node:test'
|
|
3
|
-
import { replaceExt } from './fs-utils.js'
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
describe('replaceExt', () => {
|
|
7
|
-
test('replaces a simple extension', () =>
|
|
8
|
-
equal(replaceExt('file.txt', 'md'), 'file.md'))
|
|
9
|
-
|
|
10
|
-
test('replaces a multi-part extension', () =>
|
|
11
|
-
equal(replaceExt('archive.tar.gz', 'zip'), 'archive.tar.zip'))
|
|
12
|
-
|
|
13
|
-
test('adds extension when none exists', () =>
|
|
14
|
-
equal(replaceExt('README', 'md'), 'README.md'))
|
|
15
|
-
|
|
16
|
-
test('handles empty filename', () =>
|
|
17
|
-
equal(replaceExt('', 'ext'), '.ext'))
|
|
18
|
-
|
|
19
|
-
test('handles dot-files', () =>
|
|
20
|
-
equal(replaceExt('.env', 'txt'), '.env.txt'))
|
|
21
|
-
})
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
|
-
import { tmpdir } from 'node:os'
|
|
3
|
-
import { equal, deepEqual } from 'node:assert/strict'
|
|
4
|
-
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
|
|
5
|
-
import { test, describe, before, after } from 'node:test'
|
|
6
|
-
|
|
7
|
-
import { parseOptions } from './parseOptions.js'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
describe('parseOptions', () => {
|
|
11
|
-
let testDir
|
|
12
|
-
let inTmpDir = f => join(testDir, f)
|
|
13
|
-
const testFiles = ['file1.png', 'file2.png', 'file3.png']
|
|
14
|
-
|
|
15
|
-
before(async () => {
|
|
16
|
-
testDir = await mkdtemp(join(tmpdir(), 'parse-args-'))
|
|
17
|
-
for (const file of testFiles)
|
|
18
|
-
await writeFile(inTmpDir(file), '')
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
after(() => rm(testDir, { recursive: true }))
|
|
22
|
-
|
|
23
|
-
test('parses args and globs files', async () => {
|
|
24
|
-
const { values, files } = await parseOptions({
|
|
25
|
-
'output-dir': { type: 'string' }
|
|
26
|
-
}, {
|
|
27
|
-
args: ['--output-dir', '/tmp', inTmpDir('file[12].png')],
|
|
28
|
-
})
|
|
29
|
-
equal(values['output-dir'], '/tmp')
|
|
30
|
-
deepEqual(files, [
|
|
31
|
-
inTmpDir('file1.png'),
|
|
32
|
-
inTmpDir('file2.png')
|
|
33
|
-
])
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test('respects verbatim tokens', async () => {
|
|
37
|
-
const literal0 = 'literal-file[98].png'
|
|
38
|
-
const literal1 = 'literal-file[99].png'
|
|
39
|
-
const { files } = await parseOptions({}, {
|
|
40
|
-
args: [inTmpDir('file[12].png'), '--', literal0, literal1]
|
|
41
|
-
})
|
|
42
|
-
deepEqual(files, [
|
|
43
|
-
inTmpDir('file1.png'),
|
|
44
|
-
inTmpDir('file2.png'),
|
|
45
|
-
literal0,
|
|
46
|
-
literal1,
|
|
47
|
-
])
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
test('empty files array when no positionals', async () => {
|
|
51
|
-
const { files, values } = await parseOptions({
|
|
52
|
-
foo: { type: 'boolean' }
|
|
53
|
-
}, {
|
|
54
|
-
args: ['--foo'],
|
|
55
|
-
})
|
|
56
|
-
equal(values.foo, true)
|
|
57
|
-
deepEqual(files, [])
|
|
58
|
-
})
|
|
59
|
-
})
|
package/src/vconcat.test.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { ok } from 'node:assert/strict'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { test } from 'node:test'
|
|
4
|
-
import { cpSync } from 'node:fs'
|
|
5
|
-
|
|
6
|
-
import { videoAttrs } from './utils/ffmpeg.js'
|
|
7
|
-
import { mkTempDir, cli } from './utils/test-utils.js'
|
|
8
|
-
|
|
9
|
-
const rel = f => join(import.meta.dirname, f)
|
|
10
|
-
|
|
11
|
-
test('vconcat concatenates videos with single quotes in filenames', async () => {
|
|
12
|
-
const tmp = mkTempDir('vconcat')
|
|
13
|
-
|
|
14
|
-
const file1 = join(tmp, `video'1.mp4`)
|
|
15
|
-
const file2 = join(tmp, `video'2.mp4`)
|
|
16
|
-
cpSync(rel('fixtures/60fps.mp4'), file1)
|
|
17
|
-
cpSync(rel('fixtures/60fps.mp4'), file2)
|
|
18
|
-
|
|
19
|
-
cli('vconcat', file1, file2)
|
|
20
|
-
|
|
21
|
-
const { duration } = await videoAttrs(join(tmp, `video'1.concat.mp4`))
|
|
22
|
-
ok(parseFloat(duration) === 60, `Duration should be 60s, got ${duration}s`)
|
|
23
|
-
})
|
package/src/vsplit.test.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { ok } from 'node:assert/strict'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { describe, test } from 'node:test'
|
|
4
|
-
import { cpSync, readdirSync } from 'node:fs'
|
|
5
|
-
|
|
6
|
-
import { videoAttrs } from './utils/ffmpeg.js'
|
|
7
|
-
import { mkTempDir, cli } from './utils/test-utils.js'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const rel = f => join(import.meta.dirname, f)
|
|
11
|
-
|
|
12
|
-
describe('vsplit splits video into multiple clips from CSV', () => {
|
|
13
|
-
const tmp = mkTempDir('vsplit')
|
|
14
|
-
|
|
15
|
-
const csvFile = join(tmp, '60fps.csv')
|
|
16
|
-
const inputFile = join(tmp, '60fps.mp4')
|
|
17
|
-
|
|
18
|
-
cpSync(rel('fixtures/60fps.csv'), csvFile)
|
|
19
|
-
cpSync(rel('fixtures/60fps.mp4'), inputFile)
|
|
20
|
-
cli('vsplit', csvFile, inputFile)
|
|
21
|
-
|
|
22
|
-
test('all 6 clips were created', () => {
|
|
23
|
-
const files = readdirSync(tmp).filter(f => f.startsWith('60fps_'))
|
|
24
|
-
ok(files.length === 6, `Expected 6 clips, got ${files.length}`)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
test('first clip has correct duration (5 seconds)', async () => {
|
|
28
|
-
const { duration } = await videoAttrs(join(tmp, '60fps_1.mp4'))
|
|
29
|
-
const EPSILON = 0.05
|
|
30
|
-
ok(Math.abs(parseFloat(duration) - 5) < EPSILON, `Duration should be 5s, got ${duration}s`)
|
|
31
|
-
})
|
|
32
|
-
})
|