Adds a per-job scratch directory under the new `paths.work` config (default `./work`) so audio.<n>.wav, audio.<n>.opus and output.mkv no longer live beside the user's sources in paths.input. The work subdir is named after the input basename and unconditionally removed when processFile returns (success or failure), which kills bug #4 (intermediates leaking into the user-owned input folder; output.mkv getting re-picked by the 15-second watcher tick on rename failure; fixed-name collision risk for any future concurrency). Tightens the failure paths in main.processFile too (bug #25): - mover.MoveToFailed return values are now surfaced via a new failToFailed helper. - The helper os.Stats the source first; missing -> log and skip instead of the previous silent no-op when MoveToFailed was called on output.mkv before it existed. - On rename-output failure, the source is now routed to paths.failed (it was previously left in paths.input, causing an infinite re-encode loop on the next watcher tick). The old MoveToFailed on the work-dir output is dropped — the deferred RemoveAll covers it. Mechanical changes: - PathsConfig gains `Work string \`yaml:"work"\`` with default ./work, included in EnsureDirs. - Encoder.Transcode signature now takes workDir; extractAudio, encodeOpus and encodeVideo all write into workDir. The internal cleanupWavs/cleanupOpus defers are gone (RemoveAll in main is the one cleanup path). - MANUAL.md updated: example config, field reference, §6 pipeline step wording, §9 failure handling description, §11 runtime directories block.
13 KiB
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):
ffmpegffprobeopusenc
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
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):
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"
work: "./work"
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 files go on failure | ./failed |
paths.work |
Scratch directory for per-job intermediates (wav/opus/output.mkv); deleted after every job | ./work |
All five directories are created on startup if they don't exist.
4. Running
./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.mkvafter a successful encode instead of moving it tooriginals/.-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):
dvdbluraywebdltvrip
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. A per-job scratch subdirectory under paths.work (named after the input base name without .mkv) holds all intermediates, and is deleted unconditionally at the end of the job. Any error sends the source file to paths.failed; the work subdirectory is wiped regardless of outcome.
- Parse filename → determines whether this is a movie or series, and what IDs to use.
- 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 theMulti frame detection: TFF: a BFF: b Progressive: c Undetermined: dsummary line. The source is treated as interlaced only whena+b > c; undetermined frames are ignored, and a missing summary line defaults to progressive.
- Picks the first stream with
- Probe stream languages with
ffprobe— collectslanguagetags for every audio/subtitle stream so they can be re-applied after encoding (FFmpeg's-map_metadata -1strips them otherwise). - Detect media type:
- First, check the filename for a
dvd/bluray/webdl/tvriptoken (case-insensitive). If present, that wins. - Otherwise, fall back to pixel count:
width × height < 600,000→ DVD, else Blu-ray. The chosen profile selects whichencoding.<type>.crf/encoding.<type>.presetpair from the config to use, and is written into theORIGINAL_MEDIA_TYPEmetadata tag.
- First, check the filename for a
- 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.
- Extract audio — one PCM wav per source audio stream, written to the per-job work directory as
audio.0.wav,audio.1.wav, … in source order (PCM s16le, 48 kHz). Errors out if the source has no audio streams. - Encode audio — each wav is converted with
opusenc --bitrate 128kto a matchingaudio.<n>.opusin the same work directory. - Calculate display width from SAR. The width is rescaled so the output has square pixels only when there's actually work to do —
zscaleis skipped entirely for square-pixel sources (SAR1: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). - Encode video with FFmpeg:
- Video filter chain is built conditionally.
bwdif=mode=0:par=-1:-1is prepended when the source is interlaced; thezscalestep is appended only when a rescale is actually needed (see step 8). If neither applies,-vfis omitted entirely. - Codec:
libsvtav1,-pix_fmt yuv420p10le. -crfand-presetfrom 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 -1strips 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: alsoCOLLECTION,SEASON,EPISODE,TVMAZE_ID. Values are unquoted (literal value, no wrapping"…"). - Output written to
output.mkvin the per-job work directory.
- Video filter chain is built conditionally.
- Clean up the entire per-job work directory (intermediates and
output.mkvtogether) once the rename/move below succeeds — or, on any failure, when the deferred cleanup runs. - Rename and move
output.mkvfrom the work directory topaths.outputwith 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 keepsa–z A–Z 0–9 - ä ö Ä Öonly.
- Series with a known show name →
- Dispose of the source:
-dflag 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_TYPEtag 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>.mkvfilename (only theIMDBIDmetadata 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), withtimestamp,level,message, optionalerrorandfilefields.
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
.mkvis moved topaths.failed(the move itself isos.Stat-guarded — if the source is already gone, the move is skipped and logged; if the move itself errors, that error is logged too). This guarantees the source leavespaths.inputon every failure path, so the watcher doesn't retry the same file on the next tick. - The per-job work directory under
paths.work(containing partial wav/opus/output.mkv) is deleted unconditionally. - The error is logged to
error_*.logandstructured.jsonwith 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
*.mkvdirectly inpaths.inputare picked up — no recursion. - Files are processed sequentially, one at a time, in the order
filepath.Globreturns 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 andmvit 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, …)
Runtime directories (from paths.* in the config):
paths.input User drops new .mkv files here; watcher polls every 15 s
paths.output Finished encodes land here under their final name
paths.originals Successfully-encoded sources end up here (unless -d)
paths.failed Sources of failed jobs end up here
paths.work Per-job scratch subdir (basename of input); wiped per job