aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: 32f6a0c657a11d3e84612b2169546102173f772b (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
# transcode.sh

> 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.
- **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 \
    /etc/bash_completion.d/transcode.sh
# or for your user only (requires bash-completion ≥ 2.2):
install -Dm644 transcode.sh.bash-completion \
    ~/.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:

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

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 always 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
backup_dir = "backups"

[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` |
| `[output]` | `quiet` | boolean | `-q` |
| `[output]` | `verbose` | boolean | `-v` |
| `[size_report]` | `enabled` | boolean | `-s` / `--no-size-report` |
| `[size_report]` | `file` | string | `--size-report-file` |
| `[skip]` | `codecs` | array of strings | `-S` (merged with CLI) |

> **Note:** `[skip] codecs` from the config file and `--skip-codec` from the
> CLI are **merged** — both sources contribute to the skip list.

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

### 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` | Codecs to skip (comma/space/colon separated) |
| `-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`) |
| `-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

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

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