Thread context.Context from main through watcher, encoder, and metadata
clients so a Ctrl-C during a multi-hour encode immediately kills the
ffmpeg/ffprobe/opusenc children instead of waiting for them to finish.
- main: signal.NotifyContext replaces the manual sigChan + done goroutine
- watcher.Start: takes ctx, exits on ctx.Done(); processFn signature is
now func(context.Context, string) error
- encoder: Transcode and every helper (extractAudio, encodeOpus,
encodeVideo, GetMediaInfo, GetStreamLanguages, calculateZscaleWidth)
take ctx; every exec.Command becomes exec.CommandContext so the child
is SIGKILL'd on cancel
- metadata: FetchMovieMetadata, FetchSeriesMetadata, fetchTVMazeJSON
take ctx and use http.NewRequestWithContext
Mover stays ctx-free intentionally: a rename is fast enough that
mid-cancel cleanup is the next-restart's problem. processFile's
deferred RemoveAll(workDir) and failToFailed still run after cancel,
so partial output dies in the work dir and the source moves to failed/.
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.
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.
TVmaze: drop trailing slash from base URL (was producing //shows/), send
season/number as plain ints (TVmaze rejects zero-padding), check HTTP
status before decoding so 404 bodies stop being decoded as episodes, and
fetch /shows/{id} separately for show name + IMDb mapping —
episodebynumber does not honor embed=show despite what the docs imply.
OMDb: check HTTP status, and include a body snippet when Response is
non-True with an empty Error field so rate-limit and HTML failure modes
are diagnosable.
Both: assert that the fields we actually depend on come back non-empty
after decode. Catches silent field rename/removal without making us
fragile to TVmaze adding new optional fields.
main.processFile: split the nometadata fallback by branch. Series uses
Collection (legitimate shows can have no IMDb mapping); movies keep the
IMDBID/Title check.