From 1dab2befbd492c2ee5b6e3925d49bd578fa23715 Mon Sep 17 00:00:00 2001 From: Esa Kataja Date: Sat, 16 May 2026 19:42:25 +0300 Subject: [PATCH] add: user manual --- MANUAL.md | 234 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 MANUAL.md diff --git a/MANUAL.md b/MANUAL.md new file mode 100644 index 0000000..6345cc6 --- /dev/null +++ b/MANUAL.md @@ -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`. + +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` — the TVmaze show ID (case-insensitive). +- `se` — 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 → `.SE.mkv` + - Movie → `..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, …) +```