diff --git a/core/ffmpeg/ffmpeg.go b/core/ffmpeg/ffmpeg.go index af2dab647..0ff77a77a 100644 --- a/core/ffmpeg/ffmpeg.go +++ b/core/ffmpeg/ffmpeg.go @@ -441,6 +441,14 @@ var formatOutputMap = map[string]string{ "flac": "flac", } +// formatKeepsCoverArt lists the target formats whose muxer accepts an attached +// picture. opus ("Unsupported codec id in stream 1") and adts ("adts muxer does +// not support any stream of type video") reject one, so artwork is dropped there. +var formatKeepsCoverArt = map[string]bool{ + "mp3": true, + "flac": true, +} + // defaultCommands is used to detect whether a user has customized their transcoding command. var defaultCommands = func() map[string]string { m := make(map[string]string, len(consts.DefaultTranscodings)) @@ -468,6 +476,14 @@ func buildDynamicArgs(opts TranscodeOptions) []string { args = append(args, "-i", opts.FilePath) args = append(args, "-map", "0:a:0") + // Carry over embedded cover art, when the source has any. The trailing "?" + // keeps the mapping optional so sources without artwork still transcode. + // Only mp3 and flac: the opus muxer rejects the mjpeg stream and adts + // refuses any video stream, so mapping it there breaks transcoding outright. + if formatKeepsCoverArt[opts.Format] { + args = append(args, "-map", "0:v:0?", "-c:v", "copy", "-disposition:v", "attached_pic") + } + // Preserve source tags. -map_metadata 0 copies format-level tags (MP3/FLAC); // -map_metadata 0:s:a:0 copies tags from the first audio stream (OPUS/OGG). // Both are needed because the two source families store tags at different diff --git a/core/ffmpeg/ffmpeg_test.go b/core/ffmpeg/ffmpeg_test.go index 3912bc2e6..b8dc653e0 100644 --- a/core/ffmpeg/ffmpeg_test.go +++ b/core/ffmpeg/ffmpeg_test.go @@ -114,6 +114,7 @@ var _ = Describe("ffmpeg", func() { Expect(args).To(Equal([]string{ "ffmpeg", "-i", "/music/file.flac", "-map", "0:a:0", + "-map", "0:v:0?", "-c:v", "copy", "-disposition:v", "attached_pic", "-map_metadata", "0", "-map_metadata", "0:s:a:0", "-c:a", "libmp3lame", "-b:a", "256k", @@ -134,6 +135,7 @@ var _ = Describe("ffmpeg", func() { Expect(args).To(Equal([]string{ "ffmpeg", "-i", "/music/file.dsf", "-map", "0:a:0", + "-map", "0:v:0?", "-c:v", "copy", "-disposition:v", "attached_pic", "-map_metadata", "0", "-map_metadata", "0:s:a:0", "-c:a", "flac", "-ar", "48000", @@ -161,6 +163,20 @@ var _ = Describe("ffmpeg", func() { })) }) + It("does not map cover art for muxers that reject it", func() { + // The opus and adts muxers refuse a video stream outright, so mapping + // the attached picture there would break transcoding rather than + // preserve artwork. + for _, format := range []string{"opus", "aac"} { + args := buildDynamicArgs(TranscodeOptions{ + Format: format, + FilePath: "/music/file.flac", + BitRate: 128, + }) + Expect(strings.Join(args, " ")).ToNot(ContainSubstring("0:v:0?"), format) + } + }) + It("includes offset when specified", func() { args := buildDynamicArgs(TranscodeOptions{ Format: "mp3", @@ -173,6 +189,7 @@ var _ = Describe("ffmpeg", func() { "-ss", "30", "-i", "/music/file.mp3", "-map", "0:a:0", + "-map", "0:v:0?", "-c:v", "copy", "-disposition:v", "attached_pic", "-map_metadata", "0", "-map_metadata", "0:s:a:0", "-c:a", "libmp3lame", "-b:a", "192k", @@ -209,6 +226,7 @@ var _ = Describe("ffmpeg", func() { Expect(args).To(Equal([]string{ "ffmpeg", "-i", "/music/file.dsf", "-map", "0:a:0", + "-map", "0:v:0?", "-c:v", "copy", "-disposition:v", "attached_pic", "-map_metadata", "0", "-map_metadata", "0:s:a:0", "-c:a", "flac", "-sample_fmt", "s32",