mediasnacks 0.17.0 → 0.18.1
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/.zsh/completions/_mediasnacks +0 -6
- package/README.md +10 -3
- package/install-zsh-completions.sh +26 -0
- package/package.json +10 -4
- package/src/cli.js +1 -1
- package/src/prores.js +3 -2
- package/src/vtrim.js +57 -0
- package/src/vtrim.test.js +25 -7
- package/.editorconfig +0 -11
- package/.github/workflows/test.yml +0 -25
- package/Dockerfile +0 -13
- package/TODO.md +0 -7
- package/docs/macos-quick-action.png +0 -0
- package/generate-fixtures.sh +0 -11
- package/src/vtrim.sh +0 -36
package/README.md
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
# mediasnacks
|
|
2
2
|
|
|
3
|
-
Utilities
|
|
3
|
+
Utilities video and images.
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
### Install
|
|
7
7
|
**FFmpeg and Node.js must be installed.**
|
|
8
8
|
|
|
9
9
|
```shell
|
|
10
|
-
|
|
10
|
+
npm install -g mediasnacks --ignore-scripts=true
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
```shell
|
|
16
|
+
mediasnacks <command> <args>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
|
|
13
20
|
### Commands
|
|
14
21
|
- `avif` Converts images to AVIF
|
|
15
22
|
- `sqcrop` Square crops images
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
# Exit on systems without ZSH
|
|
5
|
+
zsh=$(command -v zsh) || exit 0
|
|
6
|
+
|
|
7
|
+
# Exit on non-global (npm -g) installations
|
|
8
|
+
[ "${npm_config_global:-}" = "true" ] || exit 0
|
|
9
|
+
|
|
10
|
+
src="$(cd "$(dirname "$0")" && pwd)/.zsh/completions/_mediasnacks"
|
|
11
|
+
[ -f "$src" ] || exit 0
|
|
12
|
+
|
|
13
|
+
exec "$zsh" -s "$src" << 'ZSH_EOF'
|
|
14
|
+
src="$1"
|
|
15
|
+
for dir in "${fpath[@]}"; do
|
|
16
|
+
if [ -w "$dir" ]; then
|
|
17
|
+
dst="$dir/_mediasnacks"
|
|
18
|
+
ln -sf "$src" "$dst"
|
|
19
|
+
echo "linked zsh completions: $dst -> $src"
|
|
20
|
+
exit 0
|
|
21
|
+
fi
|
|
22
|
+
done
|
|
23
|
+
echo "zsh completions: no writable fpath directory found, skipping." >&2
|
|
24
|
+
exit 0
|
|
25
|
+
ZSH_EOF
|
|
26
|
+
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mediasnacks",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Utilities for optimizing and preparing videos and images
|
|
3
|
+
"version": "0.18.1",
|
|
4
|
+
"description": "Utilities for optimizing and preparing videos and images",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Eric Fortis",
|
|
7
7
|
"type": "module",
|
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
"mediasnacks": "src/cli.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"test": "docker run --rm $(docker build -q .)"
|
|
13
|
-
|
|
12
|
+
"test": "docker run --rm $(docker build -q .)",
|
|
13
|
+
"postinstall": "sh install-zsh-completions.sh"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
".zsh",
|
|
18
|
+
"install-zsh-completions.sh"
|
|
19
|
+
]
|
|
14
20
|
}
|
package/src/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ const COMMANDS = {
|
|
|
21
21
|
vconcat: ['vconcat.sh', 'Concatenates videos'],
|
|
22
22
|
vdiff: ['vdiff.sh', 'Plays a video with the difference of two videos'],
|
|
23
23
|
vsplit: ['vsplit.js', 'Splits a video into multiple clips from CSV timestamps'],
|
|
24
|
-
vtrim: ['vtrim.
|
|
24
|
+
vtrim: ['vtrim.js', 'Trims video from start to end time'],
|
|
25
25
|
prores: ['prores.js', 'Converts video to ProRes\n'],
|
|
26
26
|
|
|
27
27
|
flattendir: ['flattendir.sh', 'Moves all files to top dir and deletes dirs'],
|
package/src/prores.js
CHANGED
|
@@ -23,8 +23,8 @@ Arguments:
|
|
|
23
23
|
<video> Video file to convert
|
|
24
24
|
|
|
25
25
|
Options:
|
|
26
|
-
--profile <n> ProRes profile (default: 3 (HQ))
|
|
27
|
-
-h, --help
|
|
26
|
+
-p, --profile <n> ProRes profile (default: 3 (422 HQ))
|
|
27
|
+
-h, --help Show this help message
|
|
28
28
|
|
|
29
29
|
Example:
|
|
30
30
|
npx mediasnacks prores video.mov
|
|
@@ -51,6 +51,7 @@ async function main() {
|
|
|
51
51
|
throw new Error('Expected 1 argument: video file. See npx mediasnacks prores --help')
|
|
52
52
|
|
|
53
53
|
const videoPath = resolve(files[0])
|
|
54
|
+
|
|
54
55
|
const { name, dir } = parse(videoPath)
|
|
55
56
|
const outputPath = join(dir, `${name}.prores.mov`)
|
|
56
57
|
|
package/src/vtrim.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { resolve, parse } from 'node:path'
|
|
4
|
+
import { parseOptions } from './utils/parseOptions.js'
|
|
5
|
+
import { ffmpeg, assertUserHasFFmpeg } from './utils/ffmpeg.js'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
const USAGE = `
|
|
9
|
+
Usage: mediasnacks vtrim [--start <time>] [--end <time>] <video>
|
|
10
|
+
|
|
11
|
+
Trims a video without re-encoding (fast, but approximate cuts).
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
-s, --start <time> Start time (e.g. 10, 00:00:10, 1:23.5). Default: beginning.
|
|
15
|
+
-e, --end <time> End time (e.g. 30, 00:00:30, 2:45.0). Default: end of video.
|
|
16
|
+
-h, --help
|
|
17
|
+
`.trim()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
await assertUserHasFFmpeg()
|
|
22
|
+
|
|
23
|
+
const { values, files } = await parseOptions({
|
|
24
|
+
start: { short: 's', type: 'string', default: '' },
|
|
25
|
+
end: { short: 'e', type: 'string', default: '' },
|
|
26
|
+
help: { short: 'h', type: 'boolean' }
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
if (values.help) {
|
|
30
|
+
console.log(USAGE)
|
|
31
|
+
process.exit(0)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!files.length)
|
|
35
|
+
throw new Error('No video specified. See npx mediasnacks vtrim --help')
|
|
36
|
+
|
|
37
|
+
for (const file of files)
|
|
38
|
+
await trim(resolve(file), values.start, values.end)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function trim(video, start, end) {
|
|
42
|
+
const { dir, name, ext } = parse(video)
|
|
43
|
+
await ffmpeg([
|
|
44
|
+
'-v', 'error',
|
|
45
|
+
'-y',
|
|
46
|
+
start ? ['-ss', start] : [],
|
|
47
|
+
end ? ['-to', end] : [],
|
|
48
|
+
'-i', video,
|
|
49
|
+
'-c', 'copy',
|
|
50
|
+
resolve(dir, `${name}.trim${ext}`)
|
|
51
|
+
].flat())
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
main().catch(err => {
|
|
55
|
+
console.error(err.message || err)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
})
|
package/src/vtrim.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ok } from 'node:assert/strict'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import { test } from 'node:test'
|
|
4
3
|
import { cpSync } from 'node:fs'
|
|
4
|
+
import { describe, test } from 'node:test'
|
|
5
5
|
|
|
6
6
|
import { videoAttrs } from './utils/ffmpeg.js'
|
|
7
7
|
import { mkTempDir, cli } from './utils/test-utils.js'
|
|
@@ -9,14 +9,32 @@ import { mkTempDir, cli } from './utils/test-utils.js'
|
|
|
9
9
|
const rel = f => join(import.meta.dirname, f)
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
describe('vtrim', () => {
|
|
13
|
+
function almostEqual(actual, expected) {
|
|
14
|
+
const EPSILON = 0.05
|
|
15
|
+
ok(Math.abs(parseFloat(actual) - expected) < EPSILON,
|
|
16
|
+
`Duration should be around ${expected}s, got ${actual}s`)
|
|
17
|
+
}
|
|
14
18
|
|
|
19
|
+
const tmp = mkTempDir('vtrim')
|
|
15
20
|
const inputFile = join(tmp, '60fps.mp4')
|
|
16
21
|
cpSync(rel('fixtures/60fps.mp4'), inputFile)
|
|
17
|
-
cli('vtrim', 5, 10, inputFile)
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
test('from start to end time', async () => {
|
|
24
|
+
cli('vtrim', '--start', 5, '--end', 11, inputFile)
|
|
25
|
+
const { duration } = await videoAttrs(join(tmp, '60fps.trim.mp4'))
|
|
26
|
+
almostEqual(duration, 6)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('start time only', async () => {
|
|
30
|
+
cli('vtrim', '--start', 5, inputFile)
|
|
31
|
+
const { duration } = await videoAttrs(join(tmp, '60fps.trim.mp4'))
|
|
32
|
+
almostEqual(duration, 25)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('end time only', async () => {
|
|
36
|
+
cli('vtrim', '--end', 11, inputFile)
|
|
37
|
+
const { duration } = await videoAttrs(join(tmp, '60fps.trim.mp4'))
|
|
38
|
+
almostEqual(duration, 11)
|
|
39
|
+
})
|
|
22
40
|
})
|
package/.editorconfig
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
- uses: docker/setup-buildx-action@v4
|
|
16
|
-
- uses: docker/build-push-action@v7
|
|
17
|
-
with:
|
|
18
|
-
context: .
|
|
19
|
-
load: true
|
|
20
|
-
tags: app-test
|
|
21
|
-
cache-from: type=gha
|
|
22
|
-
cache-to: type=gha,mode=max
|
|
23
|
-
|
|
24
|
-
- name: Tests
|
|
25
|
-
run: docker run --rm app-test
|
package/Dockerfile
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
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 package.json .
|
|
12
|
-
|
|
13
|
-
CMD ["node", "--test"]
|
package/TODO.md
DELETED
|
Binary file
|
package/generate-fixtures.sh
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
|
|
3
|
-
docker run --rm \
|
|
4
|
-
-v "$(pwd)/src/fixtures:/workspace/src/fixtures" \
|
|
5
|
-
$(docker build -q .) \
|
|
6
|
-
/bin/bash -c "
|
|
7
|
-
node --test
|
|
8
|
-
cp /tmp/avif*/lenna.avif /workspace/src/fixtures/
|
|
9
|
-
cp /tmp/edgespic*/edgespic/*.png /workspace/src/fixtures/edgespic/
|
|
10
|
-
"
|
|
11
|
-
|
package/src/vtrim.sh
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
|
|
3
|
-
if [ "$#" -ne 3 ]; then
|
|
4
|
-
cat << EOF
|
|
5
|
-
Usage:
|
|
6
|
-
$(basename $0) <start-time> <end-time> <video-file>
|
|
7
|
-
|
|
8
|
-
Examples:
|
|
9
|
-
$(basename $0) 10 30 input.mp4
|
|
10
|
-
$(basename $0) 00:00:10 00:00:30 input.mkv
|
|
11
|
-
$(basename $0) 1:23.5 2:45.0 video.mov
|
|
12
|
-
EOF
|
|
13
|
-
exit 1
|
|
14
|
-
fi
|
|
15
|
-
|
|
16
|
-
START="$1"
|
|
17
|
-
END="$2"
|
|
18
|
-
VIDEO="$3"
|
|
19
|
-
|
|
20
|
-
if [ ! -f "$VIDEO" ]; then
|
|
21
|
-
echo "Error: file not found: $VIDEO"
|
|
22
|
-
exit 1
|
|
23
|
-
fi
|
|
24
|
-
|
|
25
|
-
BASENAME=$(basename "$VIDEO")
|
|
26
|
-
DIRNAME=$(dirname "$VIDEO")
|
|
27
|
-
EXT="${BASENAME##*.}"
|
|
28
|
-
NAME="${BASENAME%.*}"
|
|
29
|
-
|
|
30
|
-
# For speed, we copy without re-encoding (with -ss before -i), but
|
|
31
|
-
# that means that the output isn’t going to be exact
|
|
32
|
-
ffmpeg -v error -y \
|
|
33
|
-
-ss "$START" \
|
|
34
|
-
-to "$END" \
|
|
35
|
-
-i "$VIDEO" \
|
|
36
|
-
-c copy "$DIRNAME/${NAME}.trim.$EXT"
|