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.
This commit is contained in:
+78
-53
@@ -11,6 +11,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"videnc-vibe/internal/logger"
|
||||
"videnc-vibe/pkg/types"
|
||||
)
|
||||
|
||||
@@ -20,6 +21,7 @@ type Encoder struct {
|
||||
ffmpegPath string
|
||||
ffprobePath string
|
||||
opusencPath string
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
type StreamInfo struct {
|
||||
@@ -43,9 +45,48 @@ type StreamMetadata struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
func New(log *logger.Logger) *Encoder {
|
||||
return &Encoder{
|
||||
ffmpegPath: "ffmpeg",
|
||||
ffprobePath: "ffprobe",
|
||||
opusencPath: "opusenc",
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// runCmd executes the command and emits a debug entry containing the full
|
||||
// command line and its combined output. file is the source file the command
|
||||
// is acting on (may be empty).
|
||||
func (e *Encoder) runCmd(ctx context.Context, label, file, name string, args []string) ([]byte, error) {
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
extra, _ := json.Marshal(struct {
|
||||
Cmd string `json:"cmd"`
|
||||
Output string `json:"output"`
|
||||
}{
|
||||
Cmd: name + " " + strings.Join(args, " "),
|
||||
Output: string(out),
|
||||
})
|
||||
e.log.Debug(label, file, string(extra))
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (e *Encoder) CheckDeps() error {
|
||||
if _, err := exec.LookPath(e.ffmpegPath); err != nil {
|
||||
return fmt.Errorf("ffmpeg not found")
|
||||
}
|
||||
if _, err := exec.LookPath(e.ffprobePath); err != nil {
|
||||
return fmt.Errorf("ffprobe not found")
|
||||
}
|
||||
if _, err := exec.LookPath(e.opusencPath); err != nil {
|
||||
return fmt.Errorf("opusenc not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Encoder) GetStreamLanguages(ctx context.Context, path string) ([]StreamMetadata, error) {
|
||||
cmd := exec.CommandContext(ctx, e.ffprobePath, "-v", "error", "-show_streams", "-print_format", "json", path)
|
||||
output, err := cmd.CombinedOutput()
|
||||
args := []string{"-v", "error", "-show_streams", "-print_format", "json", path}
|
||||
output, err := e.runCmd(ctx, "ffprobe stream languages", path, e.ffprobePath, args)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ffprobe error: %w", err)
|
||||
}
|
||||
@@ -79,40 +120,18 @@ func (e *Encoder) GetStreamLanguages(ctx context.Context, path string) ([]Stream
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG stream languages: %+v\n", streams)
|
||||
streamsJSON, _ := json.Marshal(streams)
|
||||
e.log.Debug("parsed stream languages", path, string(streamsJSON))
|
||||
return streams, nil
|
||||
}
|
||||
|
||||
func New() *Encoder {
|
||||
return &Encoder{
|
||||
ffmpegPath: "ffmpeg",
|
||||
ffprobePath: "ffprobe",
|
||||
opusencPath: "opusenc",
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Encoder) CheckDeps() error {
|
||||
if _, err := exec.LookPath(e.ffmpegPath); err != nil {
|
||||
return fmt.Errorf("ffmpeg not found")
|
||||
}
|
||||
if _, err := exec.LookPath(e.ffprobePath); err != nil {
|
||||
return fmt.Errorf("ffprobe not found")
|
||||
}
|
||||
if _, err := exec.LookPath(e.opusencPath); err != nil {
|
||||
return fmt.Errorf("opusenc not found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Encoder) GetMediaInfo(ctx context.Context, path string) (width, height int, interlaced bool, err error) {
|
||||
cmd := exec.CommandContext(ctx, e.ffprobePath, "-v", "error", "-show_streams", "-print_format", "json", path)
|
||||
output, err := cmd.CombinedOutput()
|
||||
probeArgs := []string{"-v", "error", "-show_streams", "-print_format", "json", path}
|
||||
output, err := e.runCmd(ctx, "ffprobe media info", path, e.ffprobePath, probeArgs)
|
||||
if err != nil {
|
||||
return 0, 0, false, fmt.Errorf("ffprobe error: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG ffprobe output: %s\n", string(output))
|
||||
|
||||
var result ProbeResult
|
||||
if err := json.Unmarshal(output, &result); err != nil {
|
||||
return 0, 0, false, fmt.Errorf("parsing ffprobe output: %w", err)
|
||||
@@ -124,7 +143,10 @@ func (e *Encoder) GetMediaInfo(ctx context.Context, path string) (width, height
|
||||
width = stream.Width
|
||||
height = stream.Height
|
||||
foundVideo = true
|
||||
fmt.Printf("DEBUG detected video: %dx%d, codec=%s, SAR=%s\n", width, height, stream.CodecName, stream.SampleAspectRatio)
|
||||
info, _ := json.Marshal(map[string]interface{}{
|
||||
"width": width, "height": height, "codec": stream.CodecName, "sar": stream.SampleAspectRatio,
|
||||
})
|
||||
e.log.Debug("detected video stream", path, string(info))
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -132,7 +154,7 @@ func (e *Encoder) GetMediaInfo(ctx context.Context, path string) (width, height
|
||||
return 0, 0, false, fmt.Errorf("no video stream found")
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, e.ffmpegPath,
|
||||
idetArgs := []string{
|
||||
"-hide_banner",
|
||||
"-nostats",
|
||||
"-i", path,
|
||||
@@ -140,11 +162,12 @@ func (e *Encoder) GetMediaInfo(ctx context.Context, path string) (width, height
|
||||
"-frames:v", "400",
|
||||
"-an", "-sn",
|
||||
"-f", "null", "-",
|
||||
)
|
||||
idetOut, _ := cmd.CombinedOutput()
|
||||
}
|
||||
idetOut, _ := e.runCmd(ctx, "ffmpeg idet", path, e.ffmpegPath, idetArgs)
|
||||
interlaced = detectInterlaced(string(idetOut))
|
||||
|
||||
fmt.Printf("DEBUG interlaced: %v\n", interlaced)
|
||||
info, _ := json.Marshal(map[string]bool{"interlaced": interlaced})
|
||||
e.log.Debug("interlace detection", path, string(info))
|
||||
|
||||
return width, height, interlaced, nil
|
||||
}
|
||||
@@ -170,14 +193,12 @@ func detectInterlaced(idetOutput string) bool {
|
||||
}
|
||||
|
||||
func (e *Encoder) calculateZscaleWidth(ctx context.Context, path string, originalHeight int) (string, int, error) {
|
||||
cmd := exec.CommandContext(ctx, e.ffprobePath, "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,sample_aspect_ratio", "-print_format", "json", path)
|
||||
output, err := cmd.CombinedOutput()
|
||||
args := []string{"-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,sample_aspect_ratio", "-print_format", "json", path}
|
||||
output, err := e.runCmd(ctx, "ffprobe zscale info", path, e.ffprobePath, args)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("ffprobe error: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG ffprobe for zscale: %s\n", string(output))
|
||||
|
||||
var result ProbeResult
|
||||
if err := json.Unmarshal(output, &result); err != nil {
|
||||
return "", 0, fmt.Errorf("parsing ffprobe: %w", err)
|
||||
@@ -190,9 +211,7 @@ func (e *Encoder) calculateZscaleWidth(ctx context.Context, path string, origina
|
||||
stream := result.Streams[0]
|
||||
width := stream.Width
|
||||
height := stream.Height
|
||||
|
||||
sar := stream.SampleAspectRatio
|
||||
fmt.Printf("DEBUG stream: width=%d, height=%d, sar=%s\n", width, height, sar)
|
||||
|
||||
newWidth := width
|
||||
|
||||
@@ -212,7 +231,11 @@ func (e *Encoder) calculateZscaleWidth(ctx context.Context, path string, origina
|
||||
|
||||
// Round to nearest, then snap down to even (mod-2 widths preferred by AV1/H.264).
|
||||
newWidth = ((width*num + den/2) / den) &^ 1
|
||||
fmt.Printf("DEBUG calculated new width: %d (sar=%s)\n", newWidth, sar)
|
||||
|
||||
info, _ := json.Marshal(map[string]interface{}{
|
||||
"width": width, "height": height, "sar": sar, "new_width": newWidth,
|
||||
})
|
||||
e.log.Debug("zscale calculation", path, string(info))
|
||||
|
||||
if newWidth == width {
|
||||
return "", newWidth, nil
|
||||
@@ -263,11 +286,14 @@ func (e *Encoder) extractAudio(ctx context.Context, input, workDir string, strea
|
||||
wavs := make([]string, 0, len(audio))
|
||||
for srcAudioIndex := range audio {
|
||||
wavPath := filepath.Join(workDir, fmt.Sprintf("audio.%d.wav", srcAudioIndex))
|
||||
cmd := exec.CommandContext(ctx, e.ffmpegPath, "-i", input,
|
||||
args := []string{
|
||||
"-i", input,
|
||||
"-map", fmt.Sprintf("0:a:%d", srcAudioIndex),
|
||||
"-vn", "-c:a", "pcm_s16le", "-ar", "48000",
|
||||
"-f", "wav", wavPath)
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
"-f", "wav", wavPath,
|
||||
}
|
||||
out, err := e.runCmd(ctx, "ffmpeg extract audio", input, e.ffmpegPath, args)
|
||||
if err != nil {
|
||||
return wavs, fmt.Errorf("ffmpeg extract a:%d: %s %w", srcAudioIndex, out, err)
|
||||
}
|
||||
wavs = append(wavs, wavPath)
|
||||
@@ -280,8 +306,9 @@ func (e *Encoder) encodeOpus(ctx context.Context, wavs []string, workDir string)
|
||||
for _, wav := range wavs {
|
||||
base := filepath.Base(strings.Replace(wav, ".wav", ".opus", 1))
|
||||
out := filepath.Join(workDir, base)
|
||||
cmd := exec.CommandContext(ctx, e.opusencPath, "--bitrate", "128k", wav, out)
|
||||
if logOut, err := cmd.CombinedOutput(); err != nil {
|
||||
args := []string{"--bitrate", "128k", wav, out}
|
||||
logOut, err := e.runCmd(ctx, "opusenc", wav, e.opusencPath, args)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opusenc: %s %w", logOut, err)
|
||||
}
|
||||
opusFiles = append(opusFiles, out)
|
||||
@@ -299,8 +326,6 @@ func (e *Encoder) encodeVideo(ctx context.Context, input, workDir string, opusFi
|
||||
return fmt.Errorf("calculating zscale: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG zscale: %s (newWidth=%d)\n", zscaleStr, newWidth)
|
||||
|
||||
var filters []string
|
||||
if interlaced {
|
||||
filters = append(filters, "bwdif=mode=0:par=-1:-1")
|
||||
@@ -309,7 +334,10 @@ func (e *Encoder) encodeVideo(ctx context.Context, input, workDir string, opusFi
|
||||
filters = append(filters, zscaleStr)
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG final vf: %s\n", strings.Join(filters, ","))
|
||||
filterInfo, _ := json.Marshal(map[string]interface{}{
|
||||
"zscale": zscaleStr, "new_width": newWidth, "interlaced": interlaced, "vf": strings.Join(filters, ","),
|
||||
})
|
||||
e.log.Debug("video filter chain", input, string(filterInfo))
|
||||
|
||||
args := []string{
|
||||
"-y",
|
||||
@@ -354,7 +382,6 @@ func (e *Encoder) encodeVideo(ctx context.Context, input, workDir string, opusFi
|
||||
continue
|
||||
}
|
||||
args = append(args, fmt.Sprintf("-metadata:s:a:%d", i), fmt.Sprintf("language=%s", lang))
|
||||
fmt.Printf("DEBUG setting audio language: stream %d -> language=%s\n", i, lang)
|
||||
}
|
||||
|
||||
for _, stream := range streamLangs {
|
||||
@@ -366,7 +393,6 @@ func (e *Encoder) encodeVideo(ctx context.Context, input, workDir string, opusFi
|
||||
}
|
||||
}
|
||||
args = append(args, fmt.Sprintf("-metadata:s:s:%d", idx), fmt.Sprintf("language=%s", stream.Language))
|
||||
fmt.Printf("DEBUG setting subtitle language: stream %d -> language=%s\n", idx, stream.Language)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,9 +413,8 @@ func (e *Encoder) encodeVideo(ctx context.Context, input, workDir string, opusFi
|
||||
args = append(args, outFile)
|
||||
|
||||
args = append([]string{"-hide_banner", "-v", "error"}, args...)
|
||||
cmd := exec.CommandContext(ctx, e.ffmpegPath, args...)
|
||||
fmt.Printf("DEBUG FFmpeg command: ffmpeg %s\n", strings.Join(args, " "))
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
out, err := e.runCmd(ctx, "ffmpeg encode", input, e.ffmpegPath, args)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ffmpeg encode: %s %w", out, err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user