Files
av1dae/MANUAL.md
T
Esa Kataja 8b38986690 chore: bring MANUAL.md in line with current encoder behavior
§6 step 2: drop the codec-name allowlist, describe the bounded idet run
and the Multi-frame summary parse.

§6 step 6/7: per-stream audio.<n>.wav / audio.<n>.opus now.

§6 step 8/9: zscale is conditional and width rounds to mod-2; -vf is
omitted entirely when no filter applies. Audio language tags are indexed
by output position. TITLE/COLLECTION values are unquoted.

§6 step 11: series filename no longer requires an IMDb mapping.

§7: added a token example and notes about what the token does and does
not influence in the output filename.

§8: noted the known log-rotation limitation (filename frozen at start;
restart to rotate; structured.json does not rotate).
2026-05-16 20:20:05 +03:00

273 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# videnc-vibe — User Manual
A Go CLI that watches a folder for `.mkv` rips, transcodes them to SVT-AV1 video + Opus audio, embeds metadata fetched from OMDb (movies) or TVmaze (series), and files the results into output/originals/failed directories.
---
## 1. Requirements
External binaries must be on `$PATH` (the program checks them at startup and exits if any are missing):
- `ffmpeg`
- `ffprobe`
- `opusenc`
Go (only to build):
- Go 1.x with module support.
API keys:
- OMDb API key (free at https://www.omdbapi.com/). Required for movie metadata. TVmaze is unauthenticated.
---
## 2. Build
```bash
go build -o videnc-vibe ./cmd/videnc/
```
This produces a single static binary `./videnc-vibe`.
---
## 3. Configuration
By default the config is loaded from `~/.config/videnc-vibe/config.yaml`. Pass `-c /path/to/config.yaml` to override.
Example (`config.example.yaml`):
```yaml
omdb_api_key: "YOUR_API_KEY_HERE"
encoding:
dvd:
crf: 30
preset: 2
bluray:
crf: 29
preset: 3
webdl:
crf: 30
preset: 3
tvrip:
crf: 32
preset: 2
paths:
input: "./input"
output: "./output"
originals: "./originals"
failed: "./failed"
```
### Field reference
| Field | Meaning | Default |
|---|---|---|
| `omdb_api_key` | OMDb API key for movie metadata lookup | (none — movies won't get metadata without it) |
| `encoding.dvd.crf` | SVT-AV1 CRF for SD sources | `30` |
| `encoding.dvd.preset` | SVT-AV1 preset for SD sources | `2` |
| `encoding.bluray.crf` | SVT-AV1 CRF for HD sources | `29` |
| `encoding.bluray.preset` | SVT-AV1 preset for HD sources | `3` |
| `encoding.webdl.crf` | SVT-AV1 CRF for WebDL sources | `30` |
| `encoding.webdl.preset` | SVT-AV1 preset for WebDL sources | `3` |
| `encoding.tvrip.crf` | SVT-AV1 CRF for TVRip sources | `32` |
| `encoding.tvrip.preset` | SVT-AV1 preset for TVRip sources | `2` |
| `paths.input` | Folder polled for new `.mkv` files | `./input` |
| `paths.output` | Destination for finished encodes | `./output` |
| `paths.originals` | Where source files are moved on success (unless `-d`) | `./originals` |
| `paths.failed` | Where source + partial output go on failure | `./failed` |
All four directories are created on startup if they don't exist.
---
## 4. Running
```bash
./videnc-vibe # default config path, keep originals
./videnc-vibe -d # delete originals after successful encode
./videnc-vibe -c /etc/videnc.yaml # custom config
./videnc-vibe -c /etc/videnc.yaml -d
```
Flags:
- `-d` — delete the source `.mkv` after a successful encode instead of moving it to `originals/`.
- `-c PATH` — path to config file.
Stop with `Ctrl+C` (SIGINT) or `SIGTERM`. A signal triggers a clean shutdown after the current poll cycle.
The program runs as a foreground daemon. It scans the input directory on startup and every 15 seconds thereafter.
---
## 5. Input filename convention
The base name of each `.mkv` in `paths.input` is parsed to decide what metadata to fetch. Three forms are recognized:
### 5.1 Movies — IMDb ID
Filename must contain an IMDb tag of the form `tt<digits>`.
Examples:
```
Heat.tt0113277.mkv
some-rip-tt0114369.mkv
```
→ OMDb is queried for that IMDb ID. Title, release date, and IMDb ID are embedded.
### 5.2 Series — TVmaze ID + season/episode
Filename must contain BOTH:
- `tvm<digits>` — the TVmaze show ID (case-insensitive).
- `s<digits>e<digits>` — season and episode (case-insensitive).
Examples:
```
Breaking.Bad.tvm169.S01E01.mkv
the-wire.TVM75.s2e5.mkv
```
→ TVmaze is queried for that show/season/episode. Show name (Collection), episode title, season, episode, airdate, and the show's IMDb ID are embedded.
### 5.3 No recognizable tags
If neither pattern matches, encoding still proceeds but the file is treated as having no metadata. The output is named with a random hex string and an `.nometadata.mkv` suffix.
### 5.4 Optional: source media type
Any filename can additionally contain a media-type token (case-insensitive, word-bounded):
- `dvd`
- `bluray`
- `webdl`
- `tvrip`
Examples:
```
Heat.tt0113277.bluray.mkv
some-rip.tvm169.S01E01.webdl.mkv
old.broadcast.tvrip.tt0066026.mkv
```
The token controls **both** the `ORIGINAL_MEDIA_TYPE` metadata tag written into the output and which `encoding.<type>.crf` / `encoding.<type>.preset` pair is used.
If no token is present, the program falls back to guessing from pixel count: `width × height < 600,000` → DVD, otherwise Blu-ray. WebDL and TVRip are never auto-detected — they must be declared via the token.
---
## 6. The processing pipeline
For every `.mkv` found in `paths.input`, the program runs these steps in order. Any error sends the source file (and the partial `output.mkv`, if any) to `paths.failed`.
1. **Parse filename** → determines whether this is a movie or series, and what IDs to use.
2. **Probe video** with `ffprobe`:
- Picks the first stream with `codec_type=video`, regardless of codec name. Errors out if there isn't one.
- Records width, height, and sample aspect ratio (SAR).
- Detects interlacing by running `ffmpeg -vf idet -frames:v 400 -an -sn -f null -` and parsing the `Multi frame detection: TFF: a BFF: b Progressive: c Undetermined: d` summary line. The source is treated as interlaced only when `a+b > c`; undetermined frames are ignored, and a missing summary line defaults to progressive.
3. **Probe stream languages** with `ffprobe` — collects `language` tags for every audio/subtitle stream so they can be re-applied after encoding (FFmpeg's `-map_metadata -1` strips them otherwise).
4. **Detect media type**:
- First, check the filename for a `dvd` / `bluray` / `webdl` / `tvrip` token (case-insensitive). If present, that wins.
- Otherwise, fall back to pixel count: `width × height < 600,000` → DVD, else Blu-ray.
The chosen profile selects which `encoding.<type>.crf` / `encoding.<type>.preset` pair from the config to use, and is written into the `ORIGINAL_MEDIA_TYPE` metadata tag.
5. **Fetch metadata** from OMDb or TVmaze depending on the parsed filename. Failures here are logged but do not abort the encode — the file is just encoded without metadata.
6. **Extract audio** — one PCM wav per source audio stream, written to the input directory as `audio.0.wav`, `audio.1.wav`, … in source order (PCM s16le, 48 kHz). Errors out if the source has no audio streams.
7. **Encode audio** — each wav is converted with `opusenc --bitrate 128k` to a matching `audio.<n>.opus`.
8. **Calculate display width** from SAR. The width is rescaled so the output has square pixels only when there's actually work to do — `zscale` is skipped entirely for square-pixel sources (SAR `1:1`, `N/A`, empty, `0:N`), and for any SAR whose calculated width rounds to the source width. When rescaling, the width is rounded to the nearest even number (mod-2, preferred by AV1).
9. **Encode video** with FFmpeg:
- Video filter chain is built conditionally. `bwdif=mode=0:par=-1:-1` is prepended when the source is interlaced; the `zscale` step is appended only when a rescale is actually needed (see step 8). If neither applies, `-vf` is omitted entirely.
- Codec: `libsvtav1`, `-pix_fmt yuv420p10le`.
- `-crf` and `-preset` from the selected profile (step 4).
- `-svtav1-params film-grain=10:film-grain-denoise=1:scd=1:qm-min=4:qm-max=15:keyint=10s`.
- Streams mapped: video from input (`0:v`), subtitles from input (`0:s?` — optional), and one audio stream per Opus file (`1:a`, `2:a`, …).
- Audio re-muxed (`-c:a copy` — copies the already-Opus-encoded streams), subtitles copied (`-c:s copy`).
- `-map_metadata -1` strips global metadata; per-stream language tags are then re-applied. Audio language tags are indexed by **output position** (the position in the opus-file list), not by counting source streams, so missing-language streams don't shift the index.
- Container metadata written: `TITLE`, `DATE_RELEASED`, `IMDBID`, `ORIGINAL_MEDIA_TYPE`. For series: also `COLLECTION`, `SEASON`, `EPISODE`, `TVMAZE_ID`. Values are unquoted (literal value, no wrapping `"…"`).
- Output written to `output.mkv` in the input directory.
10. **Clean up** intermediate `audio.*.wav` and `audio.*.opus` files.
11. **Rename and move** `output.mkv` to `paths.output` with a final name:
- Series with a known show name → `<sanitized-show-name>.S<NN>E<NN>.mkv` (the series no longer needs a populated IMDb mapping — a TVmaze show with no external IMDb link still gets a useful filename).
- Movie with a known title and IMDb ID → `<sanitized-title>.<imdbID>.mkv`.
- Anything else → `<8-hex-chars>.nometadata.mkv`.
Sanitization keeps `az AZ 09 - ä ö Ä Ö` only.
12. **Dispose of the source**:
- `-d` flag set → delete the original `.mkv`.
- Otherwise → move it to `paths.originals`.
---
## 7. Output naming examples
| Input filename | Result in `paths.output` | Notes |
|---|---|---|
| `Heat.tt0113277.mkv` | `Heat.tt0113277.mkv` | Pixel-count fallback → Blu-ray profile + tag |
| `Heat.tt0113277.bluray.mkv` | `Heat.tt0113277.mkv` | Same output filename; muxed `ORIGINAL_MEDIA_TYPE=Blu-ray` is now from the token, not the guess |
| `Breaking.Bad.tvm169.S01E01.webdl.mkv` | `BreakingBad.S01E01.mkv` | WebDL profile + tag |
| `unrecognized-rip.mkv` | `a1b2c3d4.nometadata.mkv` | No IDs at all |
A few things worth knowing:
- The media-type token affects the muxed `ORIGINAL_MEDIA_TYPE` tag and the CRF/preset profile, but **not** the output filename.
- The IMDb ID written into the filename is the one returned by the API, not the one in the input filename, so a typo in the source filename will surface in the output name.
- A TVmaze show with no IMDb mapping still gets a `<Collection>.S<NN>E<NN>.mkv` filename (only the `IMDBID` metadata tag is left empty).
---
## 8. Logs
Three log files are written to the **current working directory** (not the config paths):
- `info_YYYY-MM-DD.log` — INFO messages, dated.
- `error_YYYY-MM-DD.log` — ERROR messages, dated.
- `structured.json` — one JSON object per line, every entry (info + error), with `timestamp`, `level`, `message`, optional `error` and `file` fields.
Run the program from the directory where you want the logs to land.
Note: a number of `DEBUG` lines are printed to stdout/stderr (ffprobe output, calculated zscale width, the full ffmpeg command, etc.). These are intentional but not written to the log files.
**Known limitation:** the date in `info_*.log` / `error_*.log` filenames is computed when the daemon starts and does not roll over at midnight. If the daemon is left running across days, all writes continue into the start-day's file. Restart the daemon to rotate. `structured.json` does not rotate at all.
---
## 9. Failure handling
If any step from probing through encoding through renaming fails:
- The source `.mkv` is moved to `paths.failed`.
- Any partial `output.mkv` left in the input directory is also moved to `paths.failed`.
- The error is logged to `error_*.log` and `structured.json` with the source file path.
The watcher continues with the next file; one bad rip won't stop the daemon.
---
## 10. Polling behavior
- Input directory is scanned every **15 seconds** (clamped to a 1030 s range).
- Only files matching `*.mkv` directly in `paths.input` are picked up — no recursion.
- Files are processed **sequentially**, one at a time, in the order `filepath.Glob` returns them (alphabetical on Linux).
- There is no atomic-write detection. If you're copying a large file into `paths.input`, copy it to a different name first and `mv` it into place once complete, otherwise the watcher may try to encode a half-written file.
---
## 11. Project layout
```
cmd/videnc/main.go CLI entrypoint and per-file orchestration
internal/config/ YAML config load + defaults + mkdir
internal/watcher/ Polling loop, media-type detection by pixel count
internal/encoder/ ffprobe/ffmpeg/opusenc wrapper, transcode pipeline
internal/metadata/ Filename parsing, OMDb + TVmaze clients
internal/mover/ File rename/move/delete helpers
internal/logger/ Plain + JSON logging
pkg/types/types.go Shared structs (Config, Job, Metadata, …)
```