Source kind is now declared per-file via a `dvd` / `bluray` / `webdl` / `tvrip` token in the basename (case-insensitive, word-bounded). Matches the existing `tt…` / `tvm…` filename convention. When no token is present, falls back to the pixel-count guess in DetectMediaType, which still only chooses between DVD and Blu-ray. The parsed media type drives both ORIGINAL_MEDIA_TYPE in the muxed metadata and the CRF/preset profile used for encoding. EncodingConfig gains `webdl` and `tvrip` sub-blocks with conservative defaults (WebDL 30/3, TVRip 32/2); user can override in config.yaml. Manual updated with the new tokens, config fields, and pipeline description.
266 lines
9.9 KiB
Markdown
266 lines
9.9 KiB
Markdown
# 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 video stream whose codec is `mpeg2video`, `h264`, or `hevc`.
|
||
- Records width, height, and sample aspect ratio (SAR).
|
||
- Runs `ffmpeg -vf idet` to detect interlacing (presence of `TFF`/`BFF` in stderr).
|
||
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** to `audio.wav` (PCM s16le, 48 kHz) in the input directory.
|
||
7. **Encode audio** with `opusenc --bitrate 128k` → `audio.opus`.
|
||
8. **Calculate display width** from SAR. If SAR ≠ `1:1`, the width is rescaled so the output has square pixels, using a `zscale` filter (`spline36`). Height is preserved.
|
||
9. **Encode video** with FFmpeg:
|
||
- Video filter chain: `bwdif=mode=0:par=-1:-1,zscale=w=W:h=H:filter=spline36` if interlaced, else just the `zscale` step.
|
||
- Codec: `libsvtav1`, `-pix_fmt yuv420p10le`.
|
||
- `-crf` and `-preset` from the selected profile.
|
||
- `-svtav1-params film-grain=10:film-grain-denoise=1:scd=1:qm-min=4:qm-max=15:keyint=10s`.
|
||
- Streams mapped: video from input, subtitles from input (`0:s?` — optional), audio from the re-encoded Opus files.
|
||
- Audio copied (`-c:a copy`), subtitles copied (`-c:s copy`).
|
||
- `-map_metadata -1` strips global metadata; per-stream language tags are then re-applied from the ffprobe pass.
|
||
- Container metadata written: `TITLE`, `DATE_RELEASED`, `IMDBID`, `ORIGINAL_MEDIA_TYPE`. For series: also `COLLECTION`, `SEASON`, `EPISODE`, `TVMAZE_ID`.
|
||
- Output written to `output.mkv` in the input directory.
|
||
10. **Clean up** intermediate `.wav` and `.opus` files.
|
||
11. **Rename and move** `output.mkv` to `paths.output` with a final name:
|
||
- Series → `<sanitized-show-name>.S<NN>E<NN>.mkv`
|
||
- Movie → `<sanitized-title>.<imdbID>.mkv`
|
||
- No metadata → `<8-hex-chars>.nometadata.mkv`
|
||
Sanitization keeps `a–z A–Z 0–9 - ä ö Ä Ö` 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` |
|
||
|---|---|
|
||
| `Heat.tt0113277.mkv` | `Heat.tt0113277.mkv` |
|
||
| `Breaking.Bad.tvm169.S01E01.mkv` | `BreakingBad.S01E01.mkv` |
|
||
| `unrecognized-rip.mkv` | `a1b2c3d4.nometadata.mkv` |
|
||
|
||
The IMDb ID returned by the API is used, not the one from the filename, so a typo in the filename would surface there.
|
||
|
||
---
|
||
|
||
## 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.
|
||
|
||
---
|
||
|
||
## 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 10–30 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, …)
|
||
```
|