# transcode.sh Version: 2.1.0 Revision Date: 2026-05-23 > Batch transcode helper for media files using [ffmpeg](https://ffmpeg.org/). `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](https://no-color.org/)**. ## 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 ```sh # 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/.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`:** ```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: ```sh # 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`:** ```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 ```sh # 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: ```tsv 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 ```sh # 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](LICENSES/BSD-3-Clause.txt) © 2025 Dennis Fink \