mediasnacks 0.14.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 ADDED
@@ -0,0 +1,5 @@
1
+ .git
2
+ .gitignore
3
+ .github
4
+ .DS_Store
5
+ *.log
@@ -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|vsplit|flattendir)
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
@@ -0,0 +1,5 @@
1
+ .PHONY: *
2
+
3
+ test:
4
+ @docker run --rm $$(docker build -q .)
5
+
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
@@ -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.14.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/zsh
1
+ #!/bin/sh
2
2
 
3
- # Convert to GIF
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 [[ $# -gt 0 ]]; do
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
- [[ -z "$file" ]] && usage
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
- "$outfile"
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'
@@ -1,7 +1,7 @@
1
1
  import { join } from 'node:path'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { equal, deepEqual } from 'node:assert/strict'
4
- import { mkdtemp, writeFile, rmdir } from 'node:fs/promises'
4
+ import { mkdtemp, writeFile, rm } from 'node:fs/promises'
5
5
  import { test, describe, before, after } from 'node:test'
6
6
 
7
7
  import { parseOptions } from './parseOptions.js'
@@ -18,7 +18,7 @@ describe('parseOptions', () => {
18
18
  await writeFile(inTmpDir(file), '')
19
19
  })
20
20
 
21
- after(() => rmdir(testDir, { recursive: true }))
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/zsh
1
+ #!/bin/sh
2
2
 
3
- if (( $# < 2 )); then
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
- printf "file %q\n" "$file" >> "$list_file"
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
- outfile="${first_video:r}.concat.${first_video:e}"
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,4 +1,4 @@
1
- #!/bin/zsh
1
+ #!/bin/sh
2
2
 
3
3
  if [ "$#" -ne 3 ]; then
4
4
  cat << EOF
@@ -27,9 +27,13 @@ DIRNAME=$(dirname "$VIDEO")
27
27
  EXT="${BASENAME##*.}"
28
28
  NAME="${BASENAME%.*}"
29
29
 
30
- outfile="$DIRNAME/${NAME}.trim.$EXT"
30
+ echo "start $START, end $END"
31
31
 
32
- ffmpeg -v error -y -i "$VIDEO" \
33
- -ss "$START" \
32
+ ffmpeg -v error -y \
33
+ -ss "$START" \
34
34
  -to "$END" \
35
- -c copy "$outfile"
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
@@ -1,15 +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'
5
+ import { deepEqual } from 'node:assert/strict'
6
6
  import { mkdtempSync } from 'node:fs'
7
7
 
8
- import { sha1 } from './utils.js'
8
+ import { videoAttrs } from '../src/utils/ffmpeg.js'
9
9
 
10
10
 
11
- test('PNG to AVIF', () => {
11
+ test('PNG to AVIF', async () => {
12
12
  const tmp = mkdtempSync(join(tmpdir(), 'avif-test-'))
13
- execSync(`src/cli.js avif --output-dir ${tmp} tests/fixtures/lenna.png` )
14
- equal(sha1(join(tmp, 'lenna.avif')), sha1('tests/fixtures/lenna.avif'))
13
+ execSync(`src/cli.js avif --output-dir ${tmp} ${join(import.meta.dirname, 'fixtures/lenna.png')}`)
14
+
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'
15
20
  })
Binary file
@@ -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
+ })
@@ -1,12 +1,12 @@
1
1
  import { join } from 'node:path'
2
2
  import { test } from 'node:test'
3
- import { equal } from 'node:assert/strict'
3
+ import { ok } from 'node:assert/strict'
4
4
  import { tmpdir } from 'node:os'
5
5
  import { execSync } from 'node:child_process'
6
6
  import { mkdtempSync, cpSync } from 'node:fs'
7
- import { sha1 } from './utils.js'
7
+ import { videoAttrs } from '../src/utils/ffmpeg.js'
8
8
 
9
- test('vtrim trims video from start to end time', () => {
9
+ test('vtrim trims video from start to end time', async () => {
10
10
  const tmp = mkdtempSync(join(tmpdir(), 'vtrim-test-'))
11
11
 
12
12
  const inputFile = join(tmp, '60fps.mp4')
@@ -14,7 +14,7 @@ test('vtrim trims video from start to end time', () => {
14
14
 
15
15
  execSync(`src/cli.js vtrim 5 10 ${inputFile}`)
16
16
 
17
- const out = join(tmp, '60fps.trim.mp4')
18
- const expected = 'tests/fixtures/60fps_2.mp4'
19
- equal(sha1(out), sha1(expected), 'Trimmed video (5-10s) should match 60fps_2.mp4')
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
20
  })
package/src/vsplit.sh DELETED
@@ -1,50 +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 video.mp4
10
- $(basename $0) 60 120 180 video.mov
11
- $(basename $0) 00:01:00 00:02:00 00:03:00 video.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
- DIRNAME=$(dirname "$VIDEO")
25
- BASENAME=$(basename "$VIDEO")
26
- EXT="${BASENAME##*.}"
27
- NAME="${BASENAME%.*}"
28
-
29
- N_CLIPS=$((${#SPLITS[@]} + 1))
30
-
31
- for (( i=1; i<=$N_CLIPS; i++ )); do
32
- outfile="$DIRNAME/${NAME}_${i}.$EXT"
33
-
34
- if [ $i -eq 1 ]; then # First clip: [start, first_split]
35
- ffmpeg -v error -y -i "$VIDEO" \
36
- -t "${SPLITS[1]}" \
37
- -c copy "$outfile"
38
-
39
- elif [ $i -eq $N_CLIPS ]; then # Last clip: [last_split, end]
40
- ffmpeg -v error -y -i "$VIDEO" \
41
- -ss "${SPLITS[-1]}" \
42
- -c copy "$outfile"
43
-
44
- else # Middle clip: [split[i-1], split[i]]
45
- ffmpeg -v error -y -i "$VIDEO" \
46
- -ss "${SPLITS[$((i-1))]}" \
47
- -to "${SPLITS[$i]}" \
48
- -c copy "$outfile"
49
- fi
50
- done
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,22 +0,0 @@
1
- import { join } from 'node:path'
2
- import { test } from 'node:test'
3
- import { equal } 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 { sha1 } from './utils.js'
8
-
9
- test('vsplit splits video at multiple time points', () => {
10
- const tmp = mkdtempSync(join(tmpdir(), 'vsplit-test-'))
11
-
12
- const inputFile = join(tmp, '60fps.mp4')
13
- cpSync('tests/fixtures/60fps.mp4', inputFile)
14
-
15
- execSync(`src/cli.js vsplit 5 10 15 20 25 ${inputFile}`)
16
-
17
- for (let i = 1; i <= 6; i++) {
18
- const generatedFile = join(tmp, `60fps_${i}.mp4`)
19
- const expectedFile = `tests/fixtures/60fps_${i}.mp4`
20
- equal(sha1(generatedFile), sha1(expectedFile), `60fps_${i}.mp4 hash should match expected`)
21
- }
22
- })