aboutsummaryrefslogtreecommitdiff

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>