mediasnacks 0.13.0 → 0.15.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/.dockerignore +5 -0
- package/.github/workflows/test.yml +28 -0
- package/.zsh/completions/_mediasnacks +1 -2
- package/Dockerfile +14 -0
- package/Makefile +5 -0
- package/README.md +0 -2
- package/docker-shell.sh +24 -0
- package/package.json +1 -4
- package/src/cli.js +0 -1
- package/src/gif.sh +5 -7
- package/src/sqcrop.js +1 -1
- package/src/utils/parseOptions.test.js +2 -2
- package/src/vconcat.sh +10 -6
- package/src/vtrim.sh +14 -7
- package/tests/avif.test.js +11 -13
- package/tests/fixtures/60fps.mp4 +0 -0
- package/tests/fixtures/lenna.avif +0 -0
- package/tests/utils.js +6 -0
- package/tests/vconcat.test.js +21 -0
- package/tests/vtrim.test.js +20 -0
- package/src/vsplit.sh +0 -46
package/.dockerignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on: [ push, pull_request, workflow_dispatch ]
|
|
4
|
+
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
pull-requests: write
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v6
|
|
15
|
+
|
|
16
|
+
- uses: docker/setup-buildx-action@v4
|
|
17
|
+
|
|
18
|
+
- name: Build image
|
|
19
|
+
uses: docker/build-push-action@v7
|
|
20
|
+
with:
|
|
21
|
+
context: .
|
|
22
|
+
load: true
|
|
23
|
+
tags: app-test
|
|
24
|
+
cache-from: type=gha
|
|
25
|
+
cache-to: type=gha,mode=max
|
|
26
|
+
|
|
27
|
+
- name: Run tests
|
|
28
|
+
run: docker run --rm app-test
|
|
@@ -19,7 +19,6 @@ _mediasnacks_commands=(
|
|
|
19
19
|
'rmcover:Removes cover art'
|
|
20
20
|
'curltime:Measures request response timings'
|
|
21
21
|
'gif:Video to GIF'
|
|
22
|
-
'vsplit:Split video at multiple time points'
|
|
23
22
|
'flattendir:Moves unique files to the top dir and deletes empty dirs'
|
|
24
23
|
)
|
|
25
24
|
|
|
@@ -30,7 +29,7 @@ fi
|
|
|
30
29
|
|
|
31
30
|
local cmd="$words[2]"
|
|
32
31
|
case "$cmd" in
|
|
33
|
-
avif|resize|sqcrop|moov2front|dropdups|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|vtrim|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|
|
|
32
|
+
avif|resize|sqcrop|moov2front|dropdups|seqcheck|hev1tohvc1|framediff|vdiff|vconcat|vtrim|dlaudio|dlvideo|unemoji|rmcover|curltime|gif|flattendir)
|
|
34
33
|
_files
|
|
35
34
|
;;
|
|
36
35
|
qdir)
|
package/Dockerfile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
FROM mwader/static-ffmpeg:8.0.1 AS ffmpeg
|
|
2
|
+
FROM node:24-bookworm-slim
|
|
3
|
+
|
|
4
|
+
COPY --from=ffmpeg /ffmpeg /usr/local/bin/ffmpeg
|
|
5
|
+
COPY --from=ffmpeg /ffprobe /usr/local/bin/ffprobe
|
|
6
|
+
|
|
7
|
+
ENV FORCE_COLOR=1
|
|
8
|
+
WORKDIR /workspace
|
|
9
|
+
|
|
10
|
+
COPY src/ src/
|
|
11
|
+
COPY tests/ tests/
|
|
12
|
+
COPY package.json .
|
|
13
|
+
|
|
14
|
+
CMD ["node", "--test"]
|
package/Makefile
ADDED
package/README.md
CHANGED
|
@@ -33,8 +33,6 @@ Commands:
|
|
|
33
33
|
|
|
34
34
|
- `curltime`: Measures request response timings
|
|
35
35
|
- `gif`: Video to GIF
|
|
36
|
-
- `vsplit`: Split video at multiple time points
|
|
37
|
-
|
|
38
36
|
- `flattendir`: Moves unique files to the top dir and deletes empty dirs
|
|
39
37
|
|
|
40
38
|
### Glob Patterns and Literal Filenames
|
package/docker-shell.sh
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Opens a shell in the Docker test environment with fixtures mounted
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
echo "Building Docker test image..."
|
|
7
|
+
docker build -t mediasnacks-test .
|
|
8
|
+
|
|
9
|
+
echo ""
|
|
10
|
+
echo "Opening shell in Docker container..."
|
|
11
|
+
echo "Fixtures directory is mounted at /workspace/tests/fixtures"
|
|
12
|
+
echo ""
|
|
13
|
+
echo "To generate fixtures:"
|
|
14
|
+
echo " cd /tmp"
|
|
15
|
+
echo " cp /workspace/tests/fixtures/lenna.png ."
|
|
16
|
+
echo " /workspace/src/cli.js avif lenna.png"
|
|
17
|
+
echo " sha1sum lenna.avif"
|
|
18
|
+
echo " cp lenna.avif /workspace/tests/fixtures/"
|
|
19
|
+
echo ""
|
|
20
|
+
|
|
21
|
+
docker run --rm -it \
|
|
22
|
+
-v "$(pwd)/tests/fixtures:/workspace/tests/fixtures" \
|
|
23
|
+
mediasnacks-test \
|
|
24
|
+
/bin/bash
|
package/package.json
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mediasnacks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Utilities for preparing videos, images, and audio for the web",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eric Fortis",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
9
9
|
"mediasnacks": "src/cli.js"
|
|
10
|
-
},
|
|
11
|
-
"scripts": {
|
|
12
|
-
"test": "node --test"
|
|
13
10
|
}
|
|
14
11
|
}
|
package/src/cli.js
CHANGED
|
@@ -30,7 +30,6 @@ const COMMANDS = {
|
|
|
30
30
|
|
|
31
31
|
curltime: ['curltime.sh', 'Measures request response timings'],
|
|
32
32
|
gif: ['gif.sh', 'Video to GIF'],
|
|
33
|
-
vsplit: ['vsplit.sh', 'Split video at multiple time points'],
|
|
34
33
|
flattendir: ['flattendir.sh', 'Moves all files to top dir and deletes dirs']
|
|
35
34
|
}
|
|
36
35
|
|
package/src/gif.sh
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#!/bin/
|
|
1
|
+
#!/bin/sh
|
|
2
2
|
|
|
3
|
-
#
|
|
3
|
+
# Converts to GIF
|
|
4
4
|
|
|
5
5
|
FPS=10
|
|
6
6
|
WIDTH=600
|
|
@@ -10,7 +10,7 @@ usage() {
|
|
|
10
10
|
exit 1
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
while [
|
|
13
|
+
while [ $# -gt 0 ]; do
|
|
14
14
|
case "$1" in
|
|
15
15
|
--fps)
|
|
16
16
|
FPS="$2";
|
|
@@ -26,10 +26,8 @@ while [[ $# -gt 0 ]]; do
|
|
|
26
26
|
esac
|
|
27
27
|
done
|
|
28
28
|
|
|
29
|
-
[
|
|
30
|
-
|
|
31
|
-
outfile="${file:r}.gif"
|
|
29
|
+
[ -z "$file" ] && usage
|
|
32
30
|
|
|
33
31
|
ffmpeg -v error -i "$file" \
|
|
34
32
|
-vf "fps=${FPS},scale=${WIDTH}:-1" \
|
|
35
|
-
"$
|
|
33
|
+
"${file%.*}.gif"
|
package/src/sqcrop.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { join } from 'node:path'
|
|
4
|
-
|
|
5
4
|
import { rename } from 'node:fs/promises'
|
|
5
|
+
|
|
6
6
|
import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
7
7
|
import { lstat, uniqueFilenameFor } from './utils/fs-utils.js'
|
|
8
8
|
import { parseOptions } from './utils/parseOptions.js'
|
|
@@ -7,7 +7,7 @@ import { test, describe, before, after } from 'node:test'
|
|
|
7
7
|
import { parseOptions } from './parseOptions.js'
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
describe('
|
|
10
|
+
describe('parseOptions', () => {
|
|
11
11
|
let testDir
|
|
12
12
|
let inTmpDir = f => join(testDir, f)
|
|
13
13
|
const testFiles = ['file1.png', 'file2.png', 'file3.png']
|
|
@@ -18,7 +18,7 @@ describe('parseArgsWithGlobs', () => {
|
|
|
18
18
|
await writeFile(inTmpDir(file), '')
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
after(() => rm(testDir))
|
|
21
|
+
after(() => rm(testDir, { recursive: true }))
|
|
22
22
|
|
|
23
23
|
test('parses args and globs files', async () => {
|
|
24
24
|
const { values, files } = await parseOptions({
|
package/src/vconcat.sh
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
#!/bin/
|
|
1
|
+
#!/bin/sh
|
|
2
2
|
|
|
3
|
-
if
|
|
3
|
+
if [ "$#" -lt 2 ]; then
|
|
4
4
|
cat << EOF
|
|
5
5
|
Usage:
|
|
6
|
-
$(basename $0) vid1.mov vid2.mov [...]
|
|
7
|
-
$(basename $0) *.mp4
|
|
6
|
+
$(basename "$0") vid1.mov vid2.mov [...]
|
|
7
|
+
$(basename "$0") *.mp4
|
|
8
8
|
EOF
|
|
9
9
|
exit 1
|
|
10
10
|
fi
|
|
11
11
|
|
|
12
12
|
list_file=$(mktemp -p .)
|
|
13
13
|
for file in "$@"; do
|
|
14
|
-
|
|
14
|
+
# Escape single quotes by replacing ' with '\''
|
|
15
|
+
escaped=$(printf '%s\n' "$file" | sed "s/'/'\\\\''/g")
|
|
16
|
+
printf "file '%s'\n" "$escaped" >> "$list_file"
|
|
15
17
|
done
|
|
16
18
|
|
|
17
19
|
first_video="$1"
|
|
18
|
-
|
|
20
|
+
name="${first_video%.*}"
|
|
21
|
+
ext="${first_video##*.}"
|
|
22
|
+
outfile="${name}.concat.${ext}"
|
|
19
23
|
|
|
20
24
|
ffmpeg -v error -f concat -safe 0 -i "$list_file" -c copy "$outfile"
|
|
21
25
|
|
package/src/vtrim.sh
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
#!/bin/
|
|
1
|
+
#!/bin/sh
|
|
2
2
|
|
|
3
3
|
if [ "$#" -ne 3 ]; then
|
|
4
|
-
|
|
4
|
+
cat << EOF
|
|
5
5
|
Usage:
|
|
6
6
|
$(basename $0) <start-time> <end-time> <video-file>
|
|
7
7
|
|
|
@@ -10,7 +10,7 @@ Examples:
|
|
|
10
10
|
$(basename $0) 00:00:10 00:00:30 input.mkv
|
|
11
11
|
$(basename $0) 1:23.5 2:45.0 video.mov
|
|
12
12
|
EOF
|
|
13
|
-
|
|
13
|
+
exit 1
|
|
14
14
|
fi
|
|
15
15
|
|
|
16
16
|
START="$1"
|
|
@@ -18,8 +18,8 @@ END="$2"
|
|
|
18
18
|
VIDEO="$3"
|
|
19
19
|
|
|
20
20
|
if [ ! -f "$VIDEO" ]; then
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
echo "Error: file not found: $VIDEO"
|
|
22
|
+
exit 1
|
|
23
23
|
fi
|
|
24
24
|
|
|
25
25
|
BASENAME=$(basename "$VIDEO")
|
|
@@ -27,6 +27,13 @@ DIRNAME=$(dirname "$VIDEO")
|
|
|
27
27
|
EXT="${BASENAME##*.}"
|
|
28
28
|
NAME="${BASENAME%.*}"
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
echo "start $START, end $END"
|
|
31
31
|
|
|
32
|
-
ffmpeg -v error -y
|
|
32
|
+
ffmpeg -v error -y \
|
|
33
|
+
-ss "$START" \
|
|
34
|
+
-to "$END" \
|
|
35
|
+
-i "$VIDEO" \
|
|
36
|
+
-c copy "$DIRNAME/${NAME}.trim.$EXT"
|
|
37
|
+
|
|
38
|
+
# For speed, we copy without re-encoding (with -ss before -i), but
|
|
39
|
+
# that means that the output isn’t going to be exact
|
package/tests/avif.test.js
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { test } from 'node:test'
|
|
3
|
-
import { equal } from 'node:assert/strict'
|
|
4
3
|
import { tmpdir } from 'node:os'
|
|
5
4
|
import { execSync } from 'node:child_process'
|
|
6
|
-
import {
|
|
7
|
-
import { mkdtempSync
|
|
5
|
+
import { deepEqual } from 'node:assert/strict'
|
|
6
|
+
import { mkdtempSync } from 'node:fs'
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
return createHash('sha1').update(readFileSync(filePath)).digest('hex')
|
|
11
|
-
}
|
|
8
|
+
import { videoAttrs } from '../src/utils/ffmpeg.js'
|
|
12
9
|
|
|
13
|
-
|
|
10
|
+
|
|
11
|
+
test('PNG to AVIF', async () => {
|
|
14
12
|
const tmp = mkdtempSync(join(tmpdir(), 'avif-test-'))
|
|
15
|
-
|
|
16
|
-
execSync(`src/cli.js avif --output-dir ${tmp} tests/fixtures/lenna.png`, {
|
|
17
|
-
cwd: process.cwd(),
|
|
18
|
-
stdio: 'inherit'
|
|
19
|
-
})
|
|
13
|
+
execSync(`src/cli.js avif --output-dir ${tmp} ${join(import.meta.dirname, 'fixtures/lenna.png')}`)
|
|
20
14
|
|
|
21
|
-
|
|
15
|
+
deepEqual(
|
|
16
|
+
await videoAttrs(join(tmp, 'lenna.avif')),
|
|
17
|
+
await videoAttrs(join(import.meta.dirname, 'fixtures/lenna.avif')))
|
|
18
|
+
// That's because we use non-deterministic avif.
|
|
19
|
+
// Claude says: avif is deterministic only when it's single-threaded: '-threads 1'
|
|
22
20
|
})
|
|
Binary file
|
|
Binary file
|
package/tests/utils.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { ok } from 'node:assert/strict'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { execSync } from 'node:child_process'
|
|
6
|
+
import { mkdtempSync, cpSync } from 'node:fs'
|
|
7
|
+
import { videoAttrs } from '../src/utils/ffmpeg.js'
|
|
8
|
+
|
|
9
|
+
test('vconcat concatenates videos with single quotes in filenames', async () => {
|
|
10
|
+
const tmp = mkdtempSync(join(tmpdir(), 'vconcat-test-'))
|
|
11
|
+
|
|
12
|
+
const file1 = join(tmp, `video'1.mp4`)
|
|
13
|
+
const file2 = join(tmp, `video'2.mp4`)
|
|
14
|
+
cpSync('tests/fixtures/60fps.mp4', file1)
|
|
15
|
+
cpSync('tests/fixtures/60fps.mp4', file2)
|
|
16
|
+
|
|
17
|
+
execSync(`src/cli.js vconcat "${file1}" "${file2}"`)
|
|
18
|
+
|
|
19
|
+
const { duration } = await videoAttrs(join(tmp, `video'1.concat.mp4`))
|
|
20
|
+
ok(parseFloat(duration) === 60, `Duration should be 60s, got ${duration}s`)
|
|
21
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import { ok } from 'node:assert/strict'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { execSync } from 'node:child_process'
|
|
6
|
+
import { mkdtempSync, cpSync } from 'node:fs'
|
|
7
|
+
import { videoAttrs } from '../src/utils/ffmpeg.js'
|
|
8
|
+
|
|
9
|
+
test('vtrim trims video from start to end time', async () => {
|
|
10
|
+
const tmp = mkdtempSync(join(tmpdir(), 'vtrim-test-'))
|
|
11
|
+
|
|
12
|
+
const inputFile = join(tmp, '60fps.mp4')
|
|
13
|
+
cpSync('tests/fixtures/60fps.mp4', inputFile)
|
|
14
|
+
|
|
15
|
+
execSync(`src/cli.js vtrim 5 10 ${inputFile}`)
|
|
16
|
+
|
|
17
|
+
const { duration } = await videoAttrs(join(tmp, '60fps.trim.mp4'))
|
|
18
|
+
const EPSILON = 0.05
|
|
19
|
+
ok(Math.abs(parseFloat(duration) - 5) < EPSILON, `Duration should be 5s, got ${duration}s`)
|
|
20
|
+
})
|
package/src/vsplit.sh
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
#!/bin/zsh
|
|
2
|
-
|
|
3
|
-
if [ "$#" -lt 2 ]; then
|
|
4
|
-
cat << EOF
|
|
5
|
-
Usage:
|
|
6
|
-
$(basename $0) <split-time-1> [split-time-2 ...] <video-file>
|
|
7
|
-
|
|
8
|
-
Examples:
|
|
9
|
-
$(basename $0) 123.45 input.mp4
|
|
10
|
-
$(basename $0) 60 120 180 video.mov
|
|
11
|
-
$(basename $0) 00:01:00 00:02:00 00:03:00 input.mkv
|
|
12
|
-
EOF
|
|
13
|
-
exit 1
|
|
14
|
-
fi
|
|
15
|
-
|
|
16
|
-
VIDEO="${@[-1]}"
|
|
17
|
-
SPLITS=("${@[1,-2]}")
|
|
18
|
-
|
|
19
|
-
if [ ! -f "$VIDEO" ]; then
|
|
20
|
-
echo "Error: file not found: $VIDEO"
|
|
21
|
-
exit 1
|
|
22
|
-
fi
|
|
23
|
-
|
|
24
|
-
BASENAME=$(basename "$VIDEO")
|
|
25
|
-
DIRNAME=$(dirname "$VIDEO")
|
|
26
|
-
EXT="${BASENAME##*.}"
|
|
27
|
-
NAME="${BASENAME%.*}"
|
|
28
|
-
|
|
29
|
-
SEGMENT_COUNT=$((${#SPLITS[@]} + 1))
|
|
30
|
-
|
|
31
|
-
for (( i=1; i<=$SEGMENT_COUNT; i++ )); do
|
|
32
|
-
OUTFILE="$DIRNAME/${NAME}_${i}.$EXT"
|
|
33
|
-
|
|
34
|
-
if [ $i -eq 1 ]; then
|
|
35
|
-
# First segment: from start to first split
|
|
36
|
-
ffmpeg -v error -y -i "$VIDEO" -t "${SPLITS[1]}" -c copy "$OUTFILE"
|
|
37
|
-
elif [ $i -eq $SEGMENT_COUNT ]; then
|
|
38
|
-
# Last segment: from last split to end
|
|
39
|
-
ffmpeg -v error -y -ss "${SPLITS[-1]}" -i "$VIDEO" -c copy "$OUTFILE"
|
|
40
|
-
else
|
|
41
|
-
# Middle segments: from split[i-1] to split[i]
|
|
42
|
-
PREV_SPLIT="${SPLITS[$((i-1))]}"
|
|
43
|
-
CURR_SPLIT="${SPLITS[$i]}"
|
|
44
|
-
ffmpeg -v error -y -ss "$PREV_SPLIT" -to "$CURR_SPLIT" -i "$VIDEO" -c copy "$OUTFILE"
|
|
45
|
-
fi
|
|
46
|
-
done
|