From 832e5568f525255684d921e3c8f34c822d2b5bb4 Mon Sep 17 00:00:00 2001 From: Esa Kataja Date: Sat, 4 Apr 2026 19:20:46 +0300 Subject: [PATCH] fix: preserve stream language metadata using ffprobe and -map_metadata -1 --- cmd/videnc/main.go | 7 +++- internal/encoder/encoder.go | 82 +++++++++++++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/cmd/videnc/main.go b/cmd/videnc/main.go index a9e5963..ee53d08 100644 --- a/cmd/videnc/main.go +++ b/cmd/videnc/main.go @@ -89,6 +89,11 @@ func processFile(inputPath string, cfg *types.Config, enc *encoder.Encoder, meta return err } + streamLangs, err := enc.GetStreamLanguages(inputPath) + if err != nil { + log.ErrorFile(inputPath, "Getting stream languages", err.Error()) + } + mediaType := watcher.DetectMediaType(width, height) crf := cfg.Encoding.DVD.CRF preset := cfg.Encoding.DVD.Preset @@ -128,7 +133,7 @@ func processFile(inputPath string, cfg *types.Config, enc *encoder.Encoder, meta DeleteOrigin: deleteOrigin, } - if err := enc.Transcode(inputPath, job, meta, interlaced); err != nil { + if err := enc.Transcode(inputPath, job, meta, interlaced, streamLangs); err != nil { log.ErrorFile(inputPath, "Transcoding", err.Error()) mover.MoveToFailed(inputPath, cfg.Paths.Failed) outputPath := filepath.Join(filepath.Dir(inputPath), "output.mkv") diff --git a/internal/encoder/encoder.go b/internal/encoder/encoder.go index 3ce082f..bd93936 100644 --- a/internal/encoder/encoder.go +++ b/internal/encoder/encoder.go @@ -30,6 +30,54 @@ type ProbeResult struct { Streams []StreamInfo `json:"streams"` } +type StreamMetadata struct { + Index int `json:"index"` + CodecType string `json:"codec_type"` + CodecName string `json:"codec_name"` + Language string `json:"tags"` + Title string `json:"title"` +} + +func (e *Encoder) GetStreamLanguages(path string) ([]StreamMetadata, error) { + cmd := exec.Command(e.ffprobePath, "-v", "error", "-show_streams", "-print_format", "json", path) + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("ffprobe error: %w", err) + } + + var result struct { + Streams []struct { + Index int `json:"index"` + CodecType string `json:"codec_type"` + CodecName string `json:"codec_name"` + Tags map[string]string `json:"tags"` + } `json:"streams"` + } + if err := json.Unmarshal(output, &result); err != nil { + return nil, fmt.Errorf("parsing ffprobe: %w", err) + } + + var streams []StreamMetadata + for _, s := range result.Streams { + lang := "" + title := "" + if s.Tags != nil { + lang = s.Tags["language"] + title = s.Tags["title"] + } + streams = append(streams, StreamMetadata{ + Index: s.Index, + CodecType: s.CodecType, + CodecName: s.CodecName, + Language: lang, + Title: title, + }) + } + + fmt.Printf("DEBUG stream languages: %+v\n", streams) + return streams, nil +} + func New() *Encoder { return &Encoder{ ffmpegPath: "ffmpeg", @@ -131,7 +179,7 @@ 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) error { +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) if err != nil { @@ -145,7 +193,7 @@ func (e *Encoder) Transcode(input string, job *types.Job, metadata *types.Metada } defer e.cleanupOpus(opusFiles) - if err := e.encodeVideo(input, opusFiles, job, metadata, interlaced); err != nil { + if err := e.encodeVideo(input, opusFiles, job, metadata, interlaced, streamLangs); err != nil { return fmt.Errorf("encoding video: %w", err) } @@ -177,7 +225,7 @@ func (e *Encoder) encodeOpus(wavs []string, dir string) ([]string, error) { return opusFiles, nil } -func (e *Encoder) encodeVideo(input string, opusFiles []string, job *types.Job, metadata *types.Metadata, interlaced bool) error { +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") @@ -221,12 +269,30 @@ func (e *Encoder) encodeVideo(input string, opusFiles []string, job *types.Job, args = append(args, "-c:a", "copy") args = append(args, "-c:s", "copy") - args = append(args, "-map_metadata", "0") + args = append(args, "-map_metadata", "-1") - args = append(args, "-metadata", fmt.Sprintf("TITLE=%s", metadata.Title)) - args = append(args, "-metadata", fmt.Sprintf("DATE_RELEASED=%s", metadata.DateReleased)) - args = append(args, "-metadata", fmt.Sprintf("IMDBID=%s", metadata.IMDBID)) - args = append(args, "-metadata", fmt.Sprintf("ORIGINAL_MEDIA_TYPE=%s", metadata.OriginalMedia)) + for _, stream := range streamLangs { + if stream.CodecType == "audio" && stream.Language != "" { + idx := 1 + for _, s := range streamLangs { + if s.CodecType == "audio" && s.Index < stream.Index { + idx++ + } + } + args = append(args, fmt.Sprintf("-metadata:s:a:%d", idx-1), fmt.Sprintf("language=%s", stream.Language)) + fmt.Printf("DEBUG setting audio language: stream %d -> language=%s\n", idx-1, stream.Language) + } + if stream.CodecType == "subtitle" && stream.Language != "" { + idx := 0 + for _, s := range streamLangs { + if s.CodecType == "subtitle" && s.Index < stream.Index { + idx++ + } + } + 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) + } + } if metadata.IsSeries { args = append(args, "-metadata", fmt.Sprintf("COLLECTION=%s", metadata.Collection))