diff --git a/consts/consts.go b/consts/consts.go index f453ac125..7eba445bd 100644 --- a/consts/consts.go +++ b/consts/consts.go @@ -166,7 +166,10 @@ var ( Name: "mp3 audio", TargetFormat: "mp3", DefaultBitRate: 192, - Command: "ffmpeg -ss %t -i %s -map 0:a:0 -map_metadata 0 -map_metadata 0:s:a:0 -b:a %bk -v 0 -f mp3 -", + // 0:v:0? also carries over embedded cover art, when the source has any. Only + // mp3 and flac are given this treatment: the opus and adts muxers reject a + // video stream outright. + Command: "ffmpeg -ss %t -i %s -map 0:a:0 -map 0:v:0? -map_metadata 0 -map_metadata 0:s:a:0 -b:a %bk -v 0 -c:v copy -disposition:v attached_pic -f mp3 -", }, { Name: "opus audio", @@ -184,7 +187,7 @@ var ( Name: "flac audio", TargetFormat: "flac", DefaultBitRate: 0, - Command: "ffmpeg -ss %t -i %s -map 0:a:0 -map_metadata 0 -map_metadata 0:s:a:0 -v 0 -c:a flac -f flac -", + Command: "ffmpeg -ss %t -i %s -map 0:a:0 -map 0:v:0? -map_metadata 0 -map_metadata 0:s:a:0 -v 0 -c:a flac -c:v copy -disposition:v attached_pic -f flac -", }, } ) diff --git a/core/ffmpeg/ffmpeg_test.go b/core/ffmpeg/ffmpeg_test.go index 0fa3de111..3912bc2e6 100644 --- a/core/ffmpeg/ffmpeg_test.go +++ b/core/ffmpeg/ffmpeg_test.go @@ -83,7 +83,7 @@ var _ = Describe("ffmpeg", func() { Describe("isDefaultCommand", func() { It("returns true for known default mp3 command", func() { - Expect(isDefaultCommand("mp3", "ffmpeg -ss %t -i %s -map 0:a:0 -map_metadata 0 -map_metadata 0:s:a:0 -b:a %bk -v 0 -f mp3 -")).To(BeTrue()) + Expect(isDefaultCommand("mp3", "ffmpeg -ss %t -i %s -map 0:a:0 -map 0:v:0? -map_metadata 0 -map_metadata 0:s:a:0 -b:a %bk -v 0 -c:v copy -disposition:v attached_pic -f mp3 -")).To(BeTrue()) }) It("returns true for known default opus command", func() { Expect(isDefaultCommand("opus", "ffmpeg -ss %t -i %s -map 0:a:0 -map_metadata 0 -map_metadata 0:s:a:0 -b:a %bk -v 0 -c:a libopus -f opus -")).To(BeTrue()) @@ -92,7 +92,7 @@ var _ = Describe("ffmpeg", func() { Expect(isDefaultCommand("aac", "ffmpeg -ss %t -i %s -map 0:a:0 -map_metadata 0 -map_metadata 0:s:a:0 -b:a %bk -v 0 -c:a aac -f adts -")).To(BeTrue()) }) It("returns true for known default flac command", func() { - Expect(isDefaultCommand("flac", "ffmpeg -ss %t -i %s -map 0:a:0 -map_metadata 0 -map_metadata 0:s:a:0 -v 0 -c:a flac -f flac -")).To(BeTrue()) + Expect(isDefaultCommand("flac", "ffmpeg -ss %t -i %s -map 0:a:0 -map 0:v:0? -map_metadata 0 -map_metadata 0:s:a:0 -v 0 -c:a flac -c:v copy -disposition:v attached_pic -f flac -")).To(BeTrue()) }) It("returns false for a custom command", func() { Expect(isDefaultCommand("mp3", "ffmpeg -i %s -b:a %bk -custom-flag -f mp3 -")).To(BeFalse()) diff --git a/db/migrations/20260731004500_keep_cover_art_in_default_transcodings.go b/db/migrations/20260731004500_keep_cover_art_in_default_transcodings.go new file mode 100644 index 000000000..25e2f8fd2 --- /dev/null +++ b/db/migrations/20260731004500_keep_cover_art_in_default_transcodings.go @@ -0,0 +1,56 @@ +package migrations + +import ( + "context" + "database/sql" + + "github.com/pressly/goose/v3" +) + +func init() { + goose.AddMigrationContext(upKeepCoverArtInDefaultTranscodings, downKeepCoverArtInDefaultTranscodings) +} + +// coverArtPairs maps the current default commands (audio stream only) to the new +// defaults that also carry over embedded cover art. Index 0 = old, index 1 = new. +// +// `-map 0:v:0?` picks up the attached picture when the source has one, and the +// trailing `?` keeps the command working for sources without artwork. `-c:v copy` +// avoids re-encoding it and `-disposition:v attached_pic` marks it as cover art +// rather than a video track. +// +// Only mp3 and flac are updated: the opus muxer rejects the mjpeg stream +// ("Unsupported codec id in stream 1") and adts refuses any video stream +// ("adts muxer does not support any stream of type video"), so adding the +// mapping there would break transcoding to those formats outright. +// +// Only rows still holding the exact unmodified default are updated, so any +// user-customized command is left untouched. +var coverArtPairs = [][2]string{ + { + "ffmpeg -ss %t -i %s -map 0:a:0 -map_metadata 0 -map_metadata 0:s:a:0 -b:a %bk -v 0 -f mp3 -", + "ffmpeg -ss %t -i %s -map 0:a:0 -map 0:v:0? -map_metadata 0 -map_metadata 0:s:a:0 -b:a %bk -v 0 -c:v copy -disposition:v attached_pic -f mp3 -", + }, + { + "ffmpeg -ss %t -i %s -map 0:a:0 -map_metadata 0 -map_metadata 0:s:a:0 -v 0 -c:a flac -f flac -", + "ffmpeg -ss %t -i %s -map 0:a:0 -map 0:v:0? -map_metadata 0 -map_metadata 0:s:a:0 -v 0 -c:a flac -c:v copy -disposition:v attached_pic -f flac -", + }, +} + +func upKeepCoverArtInDefaultTranscodings(ctx context.Context, tx *sql.Tx) error { + for _, p := range coverArtPairs { + if _, err := tx.ExecContext(ctx, `UPDATE transcoding SET command = ? WHERE command = ?`, p[1], p[0]); err != nil { + return err + } + } + return nil +} + +func downKeepCoverArtInDefaultTranscodings(ctx context.Context, tx *sql.Tx) error { + for _, p := range coverArtPairs { + if _, err := tx.ExecContext(ctx, `UPDATE transcoding SET command = ? WHERE command = ?`, p[0], p[1]); err != nil { + return err + } + } + return nil +}