Feat: 获取&写入 音频文件的 Metadata #43
@ -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
|
||||
}
|
||||
}
|
||||
|
5
go.mod
5
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
|
||||
)
|
||||
|
13
go.sum
13
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=
|
||||
|
@ -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)
|
||||
|
||||
|
90
internal/ffmpeg/meta_flac.go
Normal file
90
internal/ffmpeg/meta_flac.go
Normal file
@ -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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user