transcode.sh
Version: 2.1.0 Revision Date: 2026-05-23
Batch transcode helper for media files using ffmpeg.
transcode.sh re-encodes media files in place using user-defined presets —
small shell snippets that supply the ffmpeg arguments. It probes each input
file to detect the video codec and pixel format, skips files whose codec is on
the skip list, selects an appropriate output pixel format automatically, and
replaces the original atomically on success, unless --output-dir is used to
write encoded copies elsewhere.
Features
- Preset-driven — encoding parameters live in plain shell files, not hard-coded flags.
- Batch processing — pass files on the command line or via a list file
(
-f). - Codec skip list — per-user and per-invocation lists of codecs to leave untouched (e.g. skip files already encoded as AV1).
- Only-if-smaller mode (
-l) — discard the re-encoded file if it is larger than the original. - Size report (
-s) — print per-file size feedback, show a batch summary, and write a status TSV log for encoded, skipped, and failed files. - Dry-run (
-n) — preview what would happen without touching any files. - Selectable video probe stream (
--video-stream) — choose which input video stream is probed for preset helper variables. - Backup original (
--backup-dir DIR) — copy originals to an existing backup directory before replacing them. - No-replacement output mode (
--output-dir DIR) — write encoded files to a separate existing directory without replacing or moving originals. - Nice — runs ffmpeg at niceness 19 by default to avoid starving other
processes; configurable with
-N. - Respects NO_COLOR.
Requirements
| Tool | Required | Notes |
|---|---|---|
ffmpeg |
always | encoding |
ffprobe |
always | codec/format detection |
nice |
always | process priority control |
tomlq |
only when config.toml is present |
TOML config parsing |
Installation
# Clone the repo
git clone https://codeberg.org/yourname/transcode.sh.git
cd transcode.sh
# Make the script executable
chmod +x transcode.sh
# Optional: install to a directory in your PATH
install -Dm755 transcode.sh ~/.local/bin/transcode.sh
# Optional: install the man page
install -Dm644 transcode.1 ~/.local/share/man/man1/transcode.1
# Optional: install bash completion
install -Dm644 transcode.sh.bash-completion.sh \
/etc/bash_completion.d/transcode.sh
# or for your user only (requires bash-completion ≥ 2.2):
install -Dm644 transcode.sh.bash-completion.sh \
~/.local/share/bash-completion/completions/transcode.sh
Configuration
Presets
Security: presets are executed as shell code. Only use presets from trusted sources.
Presets live in:
${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/presets/<name>.sh
Each preset is sourced as Bash and must define an array named ffargs. The
following variables are available to presets at source time. They are derived
from the selected video probe stream, which defaults to v:0 and can be
changed with --video-stream or [encoding] video_stream:
| Variable | Description |
|---|---|
input_codec |
Video codec of the input file (e.g. h264, hevc) |
input_pixel_format |
Pixel format of the input after yuvj* normalisation (e.g. yuv420p) |
output_pixel_format |
Recommended output pixel format, derived from the input to preserve chroma subsampling and bit depth |
input_frame_rate |
Raw frame rate fraction of the input file as reported by ffprobe (e.g. 30000/1001, 25/1) |
input_fps |
input_frame_rate rounded to the nearest integer (e.g. 30, 25, 60) |
output_gop_size |
Recommended GOP size derived from input_fps (≈ 5 s of video, capped at 300 frames) |
Before the preset's ffargs are appended, the script always passes
these base arguments to ffmpeg:
-map 0 # include all streams from the input
-map_metadata 0 # copy all container-level metadata
-map_chapters 0 # copy all chapter markers
-c copy # default to stream copy for every stream
--video-stream only controls which input stream is probed for helper
variables such as input_codec, input_pixel_format, input_frame_rate,
input_fps, output_pixel_format, and output_gop_size. It does not rewrite
the preset's ffargs; presets remain responsible for selecting which ffmpeg
stream to encode.
A preset therefore only needs to specify the streams it wishes to
re-encode (typically -c:v:0) and any associated encoder options.
All other streams — audio, subtitles, attachments — are passed through
unchanged unless the preset explicitly overrides them.
Example — ~/.config/transcode.sh/presets/default.sh:
ffargs=(
-c:v:0 libsvtav1
-crf 30
-preset 6
-pix_fmt "$output_pixel_format"
)
A preset may carry an optional one-line description as a shell comment:
# description: Encode to AV1 with SVT-AV1 at a balanced quality/speed trade-off
ffargs=(
-c:v:0 libsvtav1
-crf 30
-preset 6
-pix_fmt "$output_pixel_format"
)
Run transcode.sh --list-presets to enumerate all presets in the preset
directory and display their descriptions.
Configuration file
An optional TOML configuration file can be placed at:
${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/config.toml
Use --config-file FILE to load an alternative path, or --no-config to skip
configuration loading entirely. CLI flags generally take precedence over values
in the config file. The file must be valid TOML — an unparseable file is a hard
error. Requires tomlq when the file is present.
Example config.toml:
[encoding]
preset = "av1"
nice = 10
video_stream = "v:0"
[processing]
skip_codecs = ["av1", "hevc"]
backup_dir = "backups"
output_dir = "encoded"
only_if_smaller = false
verify_output = true
continue = false
[size_report]
enabled = true
file = "transcode_size_report"
[output]
verbose = false
quiet = false
| Section | Key | Type | CLI equivalent | Note |
|---|---|---|---|---|
[encoding] |
preset |
string | -p |
|
[encoding] |
nice |
integer | -N |
|
[encoding] |
hwaccel |
string or boolean | --hwaccel / --no-hwaccel |
Accepts false (disable), true (use auto), or a method string such as "vaapi" or "cuda" |
[encoding] |
video_stream |
string | --video-stream |
Selects the input video stream that is probed for preset helper variables. It defaults to "v:0". Invalid config values are ignored with a warning and the default is used. This setting does not change ffmpeg stream mapping in presets. |
[processing] |
skip_codecs |
array of strings | -S / --skip-codec-override |
Codecs defined here and codecs defined using --skip-codec from the CLI are merged by default. Use --skip-codec-override to ignore configured skip codecs and use only codecs provided through --skip-codec for that run. |
[processing] |
backup_dir |
string | --backup-dir |
Must point to an existing directory. |
[processing] |
output_dir |
string | --output-dir |
Must point to an existing directory. Encoded files are written below this directory instead of replacing originals. |
[processing] |
only_if_smaller |
boolean | --only-if-smaller / --no-only-if-smaller |
|
[processing] |
verify_output |
boolean | --verify-output / --no-verify-output |
|
[processing] |
continue |
boolean | --continue / --no-continue |
|
[size_report] |
enabled |
boolean | -s / --no-size-report |
|
[size_report] |
file |
string | --size-report-file |
|
[output] |
ffmpeg_loglevel |
string | --ffmpeg-loglevel |
|
[output] |
quiet |
boolean | -q |
|
[output] |
verbose |
boolean | -v |
Usage
transcode.sh [OPTION] [--] FILE...
Input options
| Option | Description |
|---|---|
--config-file FILE, --config-file=FILE |
Load configuration from FILE instead of the default path |
--no-config |
Do not load any configuration file |
-f PATH, --encode-file PATH, --encode-file=PATH |
Read file list from PATH (one per line) |
Encoding options
| Option | Description |
|---|---|
-p NAME, --preset NAME, --preset=NAME |
Preset to use (default: default) |
-N VALUE, --nice VALUE, --nice=VALUE |
Nice adjustment for ffmpeg (default: 19) |
--hwaccel [METHOD], --hwaccel=METHOD |
Enable hardware acceleration; METHOD defaults to auto if omitted (e.g. cuda, vaapi, videotoolbox); enabled by default |
--no-hwaccel |
Disable hardware acceleration (omit -hwaccel from ffmpeg invocation) |
--video-stream STREAM_SELECTOR, --video-stream=STREAM_SELECTOR |
Select the input video stream to probe for preset helper variables, e.g. v:0 or v:1 (default: v:0) |
Processing options
| Option | Description |
|---|---|
-S LIST, --skip-codec LIST, --skip-codec=LIST |
Add codecs to the effective skip list (comma/space/colon separated) |
--skip-codec-override |
Use only codecs from --skip-codec, ignoring [processing].skip_codecs from config |
-n, --dry-run |
Show what would be done; don't run ffmpeg |
--backup-dir DIR, --backup-dir=DIR |
Copy originals to an existing backup directory before replacing them |
--output-dir DIR, --output-dir=DIR |
Write encoded files to an existing output directory instead of replacing originals |
-l, --only-if-smaller |
Only keep the encoded output if it is smaller than the original |
--no-only-if-smaller |
Do replace original even if new file is bigger |
--verify-output |
Probe output with ffprobe before replacing original (default) |
--no-verify-output |
Skip the post-encode integrity check |
-c, --continue |
Continue to next file if ffmpeg fails |
--no-continue |
Do not continue with the next file if ffmpeg fails |
Size report options
| Option | Description |
|---|---|
-s, --size-report |
Print per-file size feedback and write a status TSV log |
--no-size-report |
Disable a size report enabled in config |
--size-report-file PATH, --size-report-file=PATH |
Size report TSV path (default: transcode_size_report) |
Display options
| Option | Description |
|---|---|
--ffmpeg-loglevel LEVEL, --ffmpeg-loglevel=LEVEL |
Specify the loglevel to pass to ffmpeg (default: fatal) |
-q, --quiet |
Suppress all output (overrides -v) |
-v, --verbose |
More detailed output |
--color / --no-color |
Force or disable colored output |
General options
| Option | Description |
|---|---|
-h, -?, --help |
Show help and exit |
--version |
Print version information |
--list-presets |
List available presets (name + description) and exit |
Examples
# Transcode a single file with the default preset
transcode.sh video.mp4
# Transcode a list of files and print/write a size report
transcode.sh --size-report --encode-file list.txt
# Dry-run with verbose output to inspect detected formats
transcode.sh --dry-run --verbose my_movie.mkv
# Keep originals in an existing backup directory after successful encodes
transcode.sh --backup-dir ~/transcode-backups *.mp4
# Write encoded copies elsewhere without replacing originals
transcode.sh --preset av1 --output-dir ./encoded video.mp4
# Use a custom preset, only keep result if smaller, skip AV1 inputs
transcode.sh --preset av1_fast --only-if-smaller --skip-codec av1 /media/films/*.mkv
# Temporarily replace the configured skip list with only HEVC
transcode.sh --skip-codec-override --skip-codec hevc input.mkv
# Run at a moderate priority and continue past failures
transcode.sh --nice 10 --continue *.mp4
# Load an alternative config file
transcode.sh --config-file ~/profiles/fast.toml input.mp4
# Probe the second video stream for preset helper variables
transcode.sh --video-stream v:1 --dry-run --verbose input.mkv
When --backup-dir is used, the directory must already exist. Backups preserve
the source file's absolute path below that directory and existing backup files
are not overwritten; numeric suffixes such as .1 and .2 are appended when
needed.
When --output-dir is used, the output directory must already exist. Originals
are never replaced, moved, or deleted. Output paths preserve the input path below
the output directory: relative inputs keep their relative path (for example
movies/a.mp4 becomes encoded/movies/a.mp4), while absolute inputs are stored
without the leading slash. Existing output files are not overwritten; numeric
suffixes such as .1 and .2 are appended when needed. --backup-dir remains
compatible with --output-dir; when both are set, originals are copied to the
backup directory and encoded files are written to the output directory.
With --only-if-smaller, output-dir mode keeps the same size policy: if the
encoded file is larger than the original, the new output is deleted and no output
copy is kept.
Size report TSV
When --size-report is enabled, the TSV log uses this schema:
status input_path output_path original_bytes new_bytes saved_pct
The status column is one of encoded, skipped_codec, skipped_larger, or
failed. input_path is always the original input path. output_path is the
final path for successful encodes. For size-based skips, it records the planned
encoded path. It may be empty when no encoded output exists. Failed rows may
leave new_bytes and saved_pct empty when no encoded output exists.
skipped_larger rows record the temporary output size and therefore usually
have a negative saved_pct; the original file is kept.
For runs with more than one input file, --size-report also prints an end
summary with encoded/skipped/failed counts, total bytes saved, and the overall
weighted saving percentage across successful encodes.
Debugging
# High-level state messages
DEBUG=1 transcode.sh input.mkv
# Full bash execution trace
TRACE=1 transcode.sh input.mkv
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Runtime failure (ffmpeg error on one or more files) |
| 2 | Usage error (unknown option, missing argument) |
| 3 | Configuration error (missing or invalid preset) |
| 127 | Missing dependency |
License
BSD-3-Clause © 2025 Dennis Fink \<me+coding@dennisfink.me>
