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 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.13.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'
@@ -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('parseArgsWithGlobs', () => {
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/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,7 +1,7 @@
1
- #!/bin/zsh
1
+ #!/bin/sh
2
2
 
3
3
  if [ "$#" -ne 3 ]; then
4
- cat << EOF
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
- exit 1
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
- echo "Error: file not found: $VIDEO"
22
- exit 1
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
- OUTFILE="$DIRNAME/${NAME}.trim.$EXT"
30
+ echo "start $START, end $END"
31
31
 
32
- ffmpeg -v error -y -ss "$START" -to "$END" -i "$VIDEO" -c copy "$OUTFILE"
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
@@ -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 { createHash } from 'node:crypto'
7
- import { mkdtempSync, readFileSync } from 'node:fs'
5
+ import { deepEqual } from 'node:assert/strict'
6
+ import { mkdtempSync } from 'node:fs'
8
7
 
9
- function sha1(filePath) {
10
- return createHash('sha1').update(readFileSync(filePath)).digest('hex')
11
- }
8
+ import { videoAttrs } from '../src/utils/ffmpeg.js'
12
9
 
13
- test('PNG to AVIF', () => {
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
- equal(sha1(join(tmp, 'lenna.avif')), sha1('tests/fixtures/lenna.avif'))
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,6 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { readFileSync } from 'node:fs'
3
+
4
+ export function sha1(filePath) {
5
+ return createHash('sha1').update(readFileSync(filePath)).digest('hex')
6
+ }
@@ -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