# 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`. 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. ### 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..crf` / `encoding..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..crf` / `encoding..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..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 → `.SE.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 → `..mkv`. - Anything else → `<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` | 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 `.SE.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 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, …) ```