add: user manual
This commit is contained in:
@@ -0,0 +1,234 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
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` |
|
||||||
|
| `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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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** from pixel count:
|
||||||
|
- `width × height < 600,000` → **DVD** profile.
|
||||||
|
- Otherwise → **Blu-ray** profile.
|
||||||
|
The chosen profile selects which CRF/preset pair from the config to use.
|
||||||
|
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, …)
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user