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.
This commit is contained in:
+13
-29
@@ -3,7 +3,6 @@ package encoder
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -222,21 +221,18 @@ func (e *Encoder) calculateZscaleWidth(path string, originalHeight int) (string,
|
||||
return zscale, newWidth, nil
|
||||
}
|
||||
|
||||
func (e *Encoder) Transcode(input string, job *types.Job, metadata *types.Metadata, interlaced bool, streamLangs []StreamMetadata) error {
|
||||
dir := filepath.Dir(input)
|
||||
audioWavs, err := e.extractAudio(input, dir, streamLangs)
|
||||
func (e *Encoder) Transcode(input, workDir string, job *types.Job, metadata *types.Metadata, interlaced bool, streamLangs []StreamMetadata) error {
|
||||
audioWavs, err := e.extractAudio(input, workDir, streamLangs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("extracting audio: %w", err)
|
||||
}
|
||||
defer e.cleanupWavs(audioWavs)
|
||||
|
||||
opusFiles, err := e.encodeOpus(audioWavs, dir)
|
||||
opusFiles, err := e.encodeOpus(audioWavs, workDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding opus: %w", err)
|
||||
}
|
||||
defer e.cleanupOpus(opusFiles)
|
||||
|
||||
if err := e.encodeVideo(input, opusFiles, job, metadata, interlaced, streamLangs); err != nil {
|
||||
if err := e.encodeVideo(input, workDir, opusFiles, job, metadata, interlaced, streamLangs); err != nil {
|
||||
return fmt.Errorf("encoding video: %w", err)
|
||||
}
|
||||
|
||||
@@ -257,7 +253,7 @@ func audioStreamsInSourceOrder(streamLangs []StreamMetadata) []StreamMetadata {
|
||||
return audio
|
||||
}
|
||||
|
||||
func (e *Encoder) extractAudio(input, dir string, streamLangs []StreamMetadata) ([]string, error) {
|
||||
func (e *Encoder) extractAudio(input, workDir string, streamLangs []StreamMetadata) ([]string, error) {
|
||||
audio := audioStreamsInSourceOrder(streamLangs)
|
||||
if len(audio) == 0 {
|
||||
return nil, fmt.Errorf("no audio streams found")
|
||||
@@ -265,7 +261,7 @@ func (e *Encoder) extractAudio(input, dir string, streamLangs []StreamMetadata)
|
||||
|
||||
wavs := make([]string, 0, len(audio))
|
||||
for srcAudioIndex := range audio {
|
||||
wavPath := filepath.Join(dir, fmt.Sprintf("audio.%d.wav", srcAudioIndex))
|
||||
wavPath := filepath.Join(workDir, fmt.Sprintf("audio.%d.wav", srcAudioIndex))
|
||||
cmd := exec.Command(e.ffmpegPath, "-i", input,
|
||||
"-map", fmt.Sprintf("0:a:%d", srcAudioIndex),
|
||||
"-vn", "-c:a", "pcm_s16le", "-ar", "48000",
|
||||
@@ -278,22 +274,22 @@ func (e *Encoder) extractAudio(input, dir string, streamLangs []StreamMetadata)
|
||||
return wavs, nil
|
||||
}
|
||||
|
||||
func (e *Encoder) encodeOpus(wavs []string, dir string) ([]string, error) {
|
||||
func (e *Encoder) encodeOpus(wavs []string, workDir string) ([]string, error) {
|
||||
var opusFiles []string
|
||||
for _, wav := range wavs {
|
||||
out := strings.Replace(wav, ".wav", ".opus", 1)
|
||||
base := filepath.Base(strings.Replace(wav, ".wav", ".opus", 1))
|
||||
out := filepath.Join(workDir, base)
|
||||
cmd := exec.Command(e.opusencPath, "--bitrate", "128k", wav, out)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return nil, fmt.Errorf("opusenc: %s %w", out, err)
|
||||
if logOut, err := cmd.CombinedOutput(); err != nil {
|
||||
return nil, fmt.Errorf("opusenc: %s %w", logOut, err)
|
||||
}
|
||||
opusFiles = append(opusFiles, out)
|
||||
}
|
||||
return opusFiles, nil
|
||||
}
|
||||
|
||||
func (e *Encoder) encodeVideo(input string, opusFiles []string, job *types.Job, metadata *types.Metadata, interlaced bool, streamLangs []StreamMetadata) error {
|
||||
dir := filepath.Dir(input)
|
||||
outFile := filepath.Join(dir, "output.mkv")
|
||||
func (e *Encoder) encodeVideo(input, workDir string, opusFiles []string, job *types.Job, metadata *types.Metadata, interlaced bool, streamLangs []StreamMetadata) error {
|
||||
outFile := filepath.Join(workDir, "output.mkv")
|
||||
|
||||
svtParams := fmt.Sprintf("film-grain=10:film-grain-denoise=1:scd=1:qm-min=4:qm-max=15:keyint=10s")
|
||||
|
||||
@@ -398,15 +394,3 @@ func (e *Encoder) encodeVideo(input string, opusFiles []string, job *types.Job,
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Encoder) cleanupWavs(files []string) {
|
||||
for _, f := range files {
|
||||
os.Remove(f)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Encoder) cleanupOpus(files []string) {
|
||||
for _, f := range files {
|
||||
os.Remove(f)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user