Commit Graph
18 Commits
Author SHA1 Message Date
Esa Kataja f39f5b1b16 add: SQLite-backed structured logging with retention
Replace JSON/file logging with a logs.db (WAL) store. Thread the logger
through the encoder and metadata client for debug instrumentation of every
ffmpeg/ffprobe/opusenc invocation and OMDb/TVmaze request. API keys are
redacted before request URLs are logged. Retention defaults to 7 days,
overridable via log_retention_days in config.
2026-06-21 17:08:23 +03:00
Esa Kataja 459ab30d94 fix: cancel in-flight encode on SIGINT/SIGTERM via context plumbing
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/.
2026-05-16 20:38:26 +03:00
Esa Kataja 706cd22b05 fix: isolate per-job intermediates in paths.work and harden failure cleanup
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.
2026-05-16 20:32:23 +03:00
Esa Kataja 022d131cd6 fix: preserve all audio tracks and align language metadata indices
Bug #5: extractAudio invoked ffmpeg without `-map`, so default stream
selection kept only one audio track from sources with multiple audio
streams (e.g. eng/fra/jpn Blu-rays). It now enumerates audio streams
from the streamLangs already fetched by the caller and runs one
`ffmpeg -map 0🅰️<n>` per stream, writing audio.<n>.wav.

Bug #11: the audio language metadata loop in encodeVideo computed the
output index by counting source-side audio streams with a lower Index,
which drifted when some source streams lacked a language tag. It now
walks opusFiles in output order and looks up the language at the
matching source-audio position via a sorted helper.

These ship together because #11 was masked by #5: when only one audio
track survived extraction, the broken index calculation never produced
a visible misalignment. Fixing #5 alone would have caused multi-track
outputs with shuffled language tags; both fixes are required to land
correct multi-track output.
2026-05-16 20:07:59 +03:00
Esa Kataja 71f7ef7bca fix: skip no-op zscale and round SAR width to nearest even
Bug #2: calculateZscaleWidth previously emitted a zscale filter even
when no rescale was needed (SAR 1:1, N/A, empty, or computed width
equal to the source). encodeVideo then unconditionally appended it to
-vf, forcing a pointless colorspace round-trip. Return an empty filter
string in those cases and build the -vf chain conditionally; omit -vf
entirely when no filters apply.

Bug #23: the SAR-to-width math used integer truncation, producing odd
or off-by-one widths (e.g. 853 instead of 854 for 32:27 at 1920), and
the guard accepted SAR 0:N which zeroed the output width. Reject
zero-numerator SARs and round to the nearest integer then mask to an
even width for AV1/H.264 mod-2 alignment.
2026-05-16 20:07:50 +03:00
Esa Kataja 5584bbca57 fix: detect video stream by codec_type instead of codec name allowlist
GetMediaInfo previously only recognized mpeg2video/h264/hevc and silently
returned 0x0 with no error for other codecs (VC-1, MPEG-4 ASP, AV1,
ProRes), causing DetectMediaType to misclassify as DVD and feeding bogus
dimensions to the zscale filter. Pick the first stream with
codec_type=video and return an explicit error if none is found.
2026-05-16 20:07:33 +03:00
Esa Kataja 0662ee3576 fix: drop literal quotes from TITLE and COLLECTION metadata
exec.Command does not invoke a shell, so the wrapping " characters
were inserted into the muxed tag value verbatim (e.g. TITLE read as
"Snatch" instead of Snatch). Use unquoted Sprintf format strings.
2026-05-16 20:07:24 +03:00
Esa Kataja 07f999a338 fix: correct interlace detection and reap idet child
The previous detector substring-matched "TFF"/"BFF" against idet's own
label text, so it returned true on every source, and `cmd.Start()` was
never paired with `Wait()`, leaving a zombie ffmpeg per file.

Run idet bounded with `-frames:v 400 -an -sn -f null -` so it completes,
use CombinedOutput so the child is reaped, and parse the "Multi frame
detection" summary line — interlaced only when TFF+BFF > Progressive.
2026-05-16 19:39:33 +03:00
Esa Kataja 4783d0e0d4 fix: restore metadata tags and sanitize output filenames 2026-04-04 19:39:17 +03:00
Esa Kataja 3f3eef5d1f add: keyint=10s svtav1 parameter 2026-04-04 19:34:55 +03:00
Esa Kataja 9d8cc17e69 fix: add -preset as separate FFmpeg argument 2026-04-04 19:27:00 +03:00
Esa Kataja 832e5568f5 fix: preserve stream language metadata using ffprobe and -map_metadata -1 2026-04-04 19:20:46 +03:00
Esa Kataja 31b5f03e09 fix: use ffprobe for SAR detection and calculate zscale width 2026-04-04 19:18:43 +03:00
Esa Kataja 4eeacfe551 fix: use consistent filenames for audio extraction 2026-04-04 19:00:30 +03:00
Esa Kataja bc58017414 fix: reorder FFmpeg args to fix input option error 2026-04-04 18:58:10 +03:00
Esa Kataja 935912e376 fix: improve FFmpeg output parsing for resolution detection 2026-04-04 18:08:49 +03:00
Esa Kataja 53adafbc40 add: add interlaced detection and bwdif deinterlacing 2026-04-04 18:08:25 +03:00
Esa Kataja 0f11c62c4d add: add encoder, mover, watcher, and logger packages 2026-04-04 18:07:37 +03:00