diff --git a/cmd/um/main.go b/cmd/um/main.go index 3224be8..989026d 100644 --- a/cmd/um/main.go +++ b/cmd/um/main.go @@ -253,7 +253,7 @@ func (p *processor) process(inputFile string, allDec []common.NewDecoderFunc) er logger.Warn("sniff cover image type failed", zap.Error(err)) } else { params.AlbumArtExt = imgExt - params.AlbumArt = bytes.NewReader(cover) + params.AlbumArt = cover } } } @@ -277,7 +277,7 @@ func (p *processor) process(inputFile string, allDec []common.NewDecoderFunc) er ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - if err := ffmpeg.UpdateAudioMetadata(ctx, outPath, params); err != nil { + if err := ffmpeg.UpdateMeta(ctx, outPath, params); err != nil { return err } } diff --git a/go.mod b/go.mod index 77ed2b1..67f7119 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,14 @@ module unlock-music.dev/cli go 1.19 require ( + github.com/go-flac/flacpicture v0.2.0 + github.com/go-flac/flacvorbis v0.1.0 + github.com/go-flac/go-flac v0.3.1 github.com/samber/lo v1.36.0 github.com/urfave/cli/v2 v2.23.6 go.uber.org/zap v1.24.0 golang.org/x/crypto v0.3.0 - golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb + golang.org/x/exp v0.0.0-20221205204356-47842c84f3db golang.org/x/text v0.5.0 unlock-music.dev/mmkv v0.0.0-20221204231432-41a75bd29939 ) diff --git a/go.sum b/go.sum index c86c908..865c2c8 100644 --- a/go.sum +++ b/go.sum @@ -4,9 +4,18 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ddliu/go-httpclient v0.5.1 h1:ys4KozrhBaGdI1yuWIFwNNILqhnMU9ozTvRNfCTorvs= +github.com/ddliu/go-httpclient v0.5.1/go.mod h1:8QVbjq00YK2f2MQyiKuWMdaKOFRcoD9VuubkNCNOuZo= +github.com/go-flac/flacpicture v0.2.0 h1:rS/ZOR/ZxlEwMf3yOPFcTAmGyoV6rDtcYdd+6CwWQAw= +github.com/go-flac/flacpicture v0.2.0/go.mod h1:M4a1J0v6B5NHsck4GA1yZg0vFQzETVPd3kuj6Ow+q9o= +github.com/go-flac/flacvorbis v0.1.0 h1:xStJfPrZ/IoA2oBUEwgrlaSf+Opo6/YuQfkqVhkP0cM= +github.com/go-flac/flacvorbis v0.1.0/go.mod h1:70N9vVkQ4Jew0oBWkwqDMIE21h7pMUtQJpnMD0js6XY= +github.com/go-flac/go-flac v0.3.1 h1:BWA7HdO67S4ZLWSVHCxsDHuedFFu5RiV/wmuhvO6Hxo= +github.com/go-flac/go-flac v0.3.1/go.mod h1:jG9IumOfAXr+7J40x0AiQIbJzXf9Y7+Zs/2CNWe4LMk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= @@ -35,8 +44,8 @@ go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb h1:QIsP/NmClBICkqnJ4rSIhnrGiGR7Yv9ZORGGnmmLTPk= -golang.org/x/exp v0.0.0-20221204150635-6dcec336b2bb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go index 467acd9..9fe9ac9 100644 --- a/internal/ffmpeg/ffmpeg.go +++ b/internal/ffmpeg/ffmpeg.go @@ -39,11 +39,18 @@ type UpdateMetadataParams struct { Meta common.AudioMeta // required - AlbumArt io.Reader // optional - AlbumArtExt string // required if AlbumArt is not nil + AlbumArt []byte // optional + AlbumArtExt string // required if AlbumArt is not nil } -func UpdateAudioMetadata(ctx context.Context, outPath string, params *UpdateMetadataParams) error { +func UpdateMeta(ctx context.Context, outPath string, params *UpdateMetadataParams) error { + if params.AudioExt == ".flac" { + return updateMetaFlac(ctx, outPath, params) + } else { + return updateMetaFFmpeg(ctx, outPath, params) + } +} +func updateMetaFFmpeg(ctx context.Context, outPath string, params *UpdateMetadataParams) error { builder := newFFmpegBuilder() out := newOutputBuilder(outPath) // output to file @@ -60,7 +67,7 @@ func UpdateAudioMetadata(ctx context.Context, outPath string, params *UpdateMeta params.AudioExt != ".wav" /* wav doesn't support attached image */ { // write cover to temp file - artPath, err := utils.WriteTempFile(params.AlbumArt, params.AlbumArtExt) + artPath, err := utils.WriteTempFile(bytes.NewReader(params.AlbumArt), params.AlbumArtExt) if err != nil { return fmt.Errorf("updateAudioMeta write temp file: %w", err) } @@ -77,6 +84,10 @@ func UpdateAudioMetadata(ctx context.Context, outPath string, params *UpdateMeta out.AddOption("disposition:v", "attached_pic") out.AddMetadata("s:v", "title", "Album cover") out.AddMetadata("s:v", "comment", "Cover (front)") + case ".mp3": + out.AddOption("codec:v", "mjpeg") + out.AddMetadata("s:v", "title", "Album cover") + out.AddMetadata("s:v", "comment", "Cover (front)") default: // other formats use default behavior } } @@ -98,6 +109,11 @@ func UpdateAudioMetadata(ctx context.Context, outPath string, params *UpdateMeta out.AddMetadata("", "artist", strings.Join(artists, " / ")) } + if params.AudioExt == ".mp3" { + out.AddOption("write_id3v1", "true") + out.AddOption("id3v2_version", "3") + } + // execute ffmpeg cmd := builder.Command(ctx) diff --git a/internal/ffmpeg/meta_flac.go b/internal/ffmpeg/meta_flac.go new file mode 100644 index 0000000..4ec6a9d --- /dev/null +++ b/internal/ffmpeg/meta_flac.go @@ -0,0 +1,90 @@ +package ffmpeg + +import ( + "context" + "mime" + "strings" + + "github.com/go-flac/flacpicture" + "github.com/go-flac/flacvorbis" + "github.com/go-flac/go-flac" + "golang.org/x/exp/slices" +) + +func updateMetaFlac(_ context.Context, outPath string, m *UpdateMetadataParams) error { + f, err := flac.ParseFile(m.Audio) + if err != nil { + return err + } + + // generate comment block + comment := flacvorbis.MetaDataBlockVorbisComment{Vendor: "unlock-music.dev"} + + // add metadata + title := m.Meta.GetTitle() + if title != "" { + _ = comment.Add(flacvorbis.FIELD_TITLE, title) + } + + album := m.Meta.GetAlbum() + if album != "" { + _ = comment.Add(flacvorbis.FIELD_ALBUM, album) + } + + artists := m.Meta.GetArtists() + for _, artist := range artists { + _ = comment.Add(flacvorbis.FIELD_ARTIST, artist) + } + + existCommentIdx := slices.IndexFunc(f.Meta, func(b *flac.MetaDataBlock) bool { + return b.Type == flac.VorbisComment + }) + if existCommentIdx >= 0 { // copy existing comment fields + exist, err := flacvorbis.ParseFromMetaDataBlock(*f.Meta[existCommentIdx]) + if err != nil { + for _, s := range exist.Comments { + if strings.HasPrefix(s, flacvorbis.FIELD_TITLE+"=") && title != "" || + strings.HasPrefix(s, flacvorbis.FIELD_ALBUM+"=") && album != "" || + strings.HasPrefix(s, flacvorbis.FIELD_ARTIST+"=") && len(artists) != 0 { + continue + } + comment.Comments = append(comment.Comments, s) + } + } + } + + // add / replace flac comment + cmtBlock := comment.Marshal() + if existCommentIdx < 0 { + f.Meta = append(f.Meta, &cmtBlock) + } else { + f.Meta[existCommentIdx] = &cmtBlock + } + + if m.AlbumArt != nil { + + cover, err := flacpicture.NewFromImageData( + flacpicture.PictureTypeFrontCover, + "Front cover", + m.AlbumArt, + mime.TypeByExtension(m.AlbumArtExt), + ) + if err != nil { + return err + } + coverBlock := cover.Marshal() + f.Meta = append(f.Meta, &coverBlock) + + // add / replace flac cover + coverIdx := slices.IndexFunc(f.Meta, func(b *flac.MetaDataBlock) bool { + return b.Type == flac.Picture + }) + if coverIdx < 0 { + f.Meta = append(f.Meta, &coverBlock) + } else { + f.Meta[coverIdx] = &coverBlock + } + } + + return f.Save(outPath) +}