fix: preserve stream language metadata using ffprobe and -map_metadata -1

This commit is contained in:
Esa Kataja
2026-04-04 19:20:46 +03:00
parent 31b5f03e09
commit 832e5568f5
2 changed files with 80 additions and 9 deletions
+6 -1
View File
@@ -89,6 +89,11 @@ func processFile(inputPath string, cfg *types.Config, enc *encoder.Encoder, meta
return err return err
} }
streamLangs, err := enc.GetStreamLanguages(inputPath)
if err != nil {
log.ErrorFile(inputPath, "Getting stream languages", err.Error())
}
mediaType := watcher.DetectMediaType(width, height) mediaType := watcher.DetectMediaType(width, height)
crf := cfg.Encoding.DVD.CRF crf := cfg.Encoding.DVD.CRF
preset := cfg.Encoding.DVD.Preset preset := cfg.Encoding.DVD.Preset
@@ -128,7 +133,7 @@ func processFile(inputPath string, cfg *types.Config, enc *encoder.Encoder, meta
DeleteOrigin: deleteOrigin, 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()) log.ErrorFile(inputPath, "Transcoding", err.Error())
mover.MoveToFailed(inputPath, cfg.Paths.Failed) mover.MoveToFailed(inputPath, cfg.Paths.Failed)
outputPath := filepath.Join(filepath.Dir(inputPath), "output.mkv") outputPath := filepath.Join(filepath.Dir(inputPath), "output.mkv")
+74 -8
View File
@@ -30,6 +30,54 @@ type ProbeResult struct {
Streams []StreamInfo `json:"streams"` 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 { func New() *Encoder {
return &Encoder{ return &Encoder{
ffmpegPath: "ffmpeg", ffmpegPath: "ffmpeg",
@@ -131,7 +179,7 @@ func (e *Encoder) calculateZscaleWidth(path string, originalHeight int) (string,
return zscale, newWidth, nil 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) dir := filepath.Dir(input)
audioWavs, err := e.extractAudio(input, dir) audioWavs, err := e.extractAudio(input, dir)
if err != nil { if err != nil {
@@ -145,7 +193,7 @@ func (e *Encoder) Transcode(input string, job *types.Job, metadata *types.Metada
} }
defer e.cleanupOpus(opusFiles) 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) return fmt.Errorf("encoding video: %w", err)
} }
@@ -177,7 +225,7 @@ func (e *Encoder) encodeOpus(wavs []string, dir string) ([]string, error) {
return opusFiles, nil 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) dir := filepath.Dir(input)
outFile := filepath.Join(dir, "output.mkv") 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:a", "copy")
args = append(args, "-c:s", "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)) for _, stream := range streamLangs {
args = append(args, "-metadata", fmt.Sprintf("DATE_RELEASED=%s", metadata.DateReleased)) if stream.CodecType == "audio" && stream.Language != "" {
args = append(args, "-metadata", fmt.Sprintf("IMDBID=%s", metadata.IMDBID)) idx := 1
args = append(args, "-metadata", fmt.Sprintf("ORIGINAL_MEDIA_TYPE=%s", metadata.OriginalMedia)) 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 { if metadata.IsSeries {
args = append(args, "-metadata", fmt.Sprintf("COLLECTION=%s", metadata.Collection)) args = append(args, "-metadata", fmt.Sprintf("COLLECTION=%s", metadata.Collection))