summaryrefslogtreecommitdiff
path: root/README.md
blob: 7257c5986bbd89cca5b0255bd7cc6e8144a4106c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
<!--
SPDX-FileCopyrightText: 2026 Dennis Fink <me+coding@dennisfink.me>

SPDX-License-Identifier: BSD-3-Clause
-->

# transcode.sh

Version: <!-- release-version -->2.0.0<!-- /release-version -->
Revision Date: <!-- revision-date -->2026-05-17<!-- /revision-date -->

> 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.

## 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.
- **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/<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`:**

```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. CLI flags generally take
precedence over values in the config file. Skip codecs are additive by default:
configured codecs and CLI codecs are merged unless `--skip-codec-override` is
used. 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
backup_dir = "backups"
video_stream = "v:0"

[skip]
codecs = ["av1", "hevc"]

[size_report]
enabled = true
file    = "transcode_size_report"

[output]
verbose = false
quiet   = false
```

| Section | Key | Type | CLI equivalent |
|---------|-----|------|----------------|
| `[encoding]` | `backup_dir` | string | `--backup-dir` |
| `[encoding]` | `hwaccel` | string or boolean | `--hwaccel` / `--no-hwaccel` |
| `[encoding]` | `nice` | integer | `-N` |
| `[encoding]` | `preset` | string | `-p` |
| `[encoding]` | `verify_output` | boolean | `--verify-output` |
| `[encoding]` | `video_stream` | string | `--video-stream` |
| `[output]` | `quiet` | boolean | `-q` |
| `[output]` | `verbose` | boolean | `-v` |
| `[output]` | `ffmpeg_loglevel` | string | `--ffmpeg-loglevel` |
| `[size_report]` | `enabled` | boolean | `-s` / `--no-size-report` |
| `[size_report]` | `file` | string | `--size-report-file` |
| `[skip]` | `codecs` | array of strings | `-S` / `--skip-codec-override` |

> **Note:** `[skip] codecs` from the config file and `--skip-codec` from the
> CLI are **merged** by default — both sources contribute to the skip list. Use
> `--skip-codec-override` to ignore configured skip codecs and use only codecs
> provided through `--skip-codec` for that run.

> **Note:** `[encoding] hwaccel` accepts `false` (disable), `true` (use `auto`),
> or a method string such as `"vaapi"` or `"cuda"`. CLI `--hwaccel`/`--no-hwaccel`
> always takes precedence.

> **Note:** `[encoding] backup_dir` must point to an existing directory. CLI
> `--backup-dir` always takes precedence.

> **Note:** `[encoding] 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. CLI
> `--video-stream` always takes precedence. This setting does not change ffmpeg
> stream mapping in presets.

### Codec skip list (deprecated)

The legacy `skip.conf` file is no longer read. If it still exists, a
deprecation warning is printed at startup. Migrate its contents to
`[skip] codecs` in `config.toml` and delete the old file.

## Usage

```
transcode.sh [OPTION] [--] FILE...
```

| Option | Description |
|--------|-------------|
| `-c`, `--continue` | Continue to next file if ffmpeg fails |
| `--no-continue` | Do not continue with the next file if ffmpeg fails |
| `--verify-output` | Probe output with ffprobe before replacing original (default) |
| `--no-verify-output` | Skip the post-encode integrity check |
| `--config-file FILE`, `--config-file=FILE` | Load configuration from FILE instead of the default path |
| `-f PATH`, `--encode-file PATH`, `--encode-file=PATH` | Read file list from PATH (one per line) |
| `-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 |
| `-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) |
| `-s`, `--size-report` | Print per-file size feedback and write a status TSV log |
| `--no-size-report` | Disable a size report enabled in `config.toml` |
| `--size-report-file PATH`, `--size-report-file=PATH` | Size report TSV path (default: `transcode_size_report`) |
| `-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 `[skip].codecs` from config |
| `-l`, `--only-if-smaller` | Only replace original if new file is smaller |
| `--no-only-if-smaller` | Do replace original even if new file is bigger |
| `-p NAME`, `--preset NAME`, `--preset=NAME` | Preset to use (default: `default`) |
| `--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`) |
| `--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 |
| `--version` | Print version information |
| `--list-presets` | List available presets (name + description) and exit |
| `-h`, `-?`, `--help` | Show help 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

# 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.

## Size report TSV

When `--size-report` is enabled, the TSV log uses this schema:

```tsv
status  filename    original_bytes  new_bytes   saved_pct
```

The `status` column is one of `encoded`, `skipped_codec`, `skipped_larger`,
or `failed`. 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 \<me+coding@dennisfink.me\>