Feat: 获取&写入 音频文件的 Metadata #43

Merged
um-dev merged 13 commits from feat/audio-meta into master 2022-12-06 17:55:19 +00:00
3 changed files with 83 additions and 5 deletions
Showing only changes of commit 112d9ab28e - Show all commits

View File

@ -34,8 +34,10 @@ type Decoder struct {
albumMediaID string albumMediaID string
// cache // cache
meta common.AudioMeta meta common.AudioMeta
cover []byte cover []byte
embeddedCover bool // embeddedCover is true if the cover is embedded in the file
probeBuf *bytes.Buffer // probeBuf is the buffer for sniffing metadata
// provider // provider
logger *zap.Logger logger *zap.Logger
@ -48,6 +50,8 @@ func (d *Decoder) Read(p []byte) (int, error) {
if n > 0 { if n > 0 {
d.cipher.Decrypt(p[:n], d.offset) d.cipher.Decrypt(p[:n], d.offset)
d.offset += n d.offset += n
_, _ = d.probeBuf.Write(p[:n]) // bytes.Buffer.Write never return error
} }
return n, err return n, err
} }
@ -89,6 +93,9 @@ func (d *Decoder) Validate() error {
} }
d.audio = io.LimitReader(d.raw, int64(d.audioLen)) d.audio = io.LimitReader(d.raw, int64(d.audioLen))
// prepare for sniffing metadata
d.probeBuf = bytes.NewBuffer(make([]byte, 0, d.audioLen))
return nil return nil
} }

View File

@ -4,9 +4,14 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"strings"
"github.com/samber/lo"
"unlock-music.dev/cli/algo/common" "unlock-music.dev/cli/algo/common"
"unlock-music.dev/cli/algo/qmc/client" "unlock-music.dev/cli/algo/qmc/client"
"unlock-music.dev/cli/internal/ffmpeg"
) )
func (d *Decoder) GetAudioMeta(ctx context.Context) (common.AudioMeta, error) { func (d *Decoder) GetAudioMeta(ctx context.Context) (common.AudioMeta, error) {
@ -15,10 +20,27 @@ func (d *Decoder) GetAudioMeta(ctx context.Context) (common.AudioMeta, error) {
} }
if d.songID != 0 { if d.songID != 0 {
return d.meta, d.getMetaBySongID(ctx) if err := d.getMetaBySongID(ctx); err != nil {
return nil, err
}
return d.meta, nil
} }
return nil, errors.New("qmc[GetAudioMeta] not implemented") embedMeta, err := ffmpeg.ProbeReader(ctx, d.probeBuf)
if err != nil {
return nil, fmt.Errorf("qmc[GetAudioMeta] probe reader: %w", err)
}
d.meta = embedMeta
d.embeddedCover = embedMeta.HasAttachedPic()
if !d.embeddedCover && embedMeta.HasMetadata() {
if err := d.searchMetaOnline(ctx, embedMeta); err != nil {
return nil, err
}
return d.meta, nil
}
return d.meta, nil
} }
func (d *Decoder) getMetaBySongID(ctx context.Context) error { func (d *Decoder) getMetaBySongID(ctx context.Context) error {
@ -38,12 +60,56 @@ func (d *Decoder) getMetaBySongID(ctx context.Context) error {
return nil return nil
} }
func (d *Decoder) searchMetaOnline(ctx context.Context, original common.AudioMeta) error {
c := client.NewQQMusicClient() // todo: use global client
keyword := lo.WithoutEmpty(append(
[]string{original.GetTitle(), original.GetAlbum()},
original.GetArtists()...),
)
if len(keyword) == 0 {
return errors.New("qmc[searchMetaOnline] no keyword")
}
trackList, err := c.Search(ctx, strings.Join(keyword, " "))
if err != nil {
return fmt.Errorf("qmc[searchMetaOnline] search: %w", err)
}
if len(trackList) == 0 {
return errors.New("qmc[searchMetaOnline] no result")
}
meta := trackList[0]
d.meta = meta
d.albumID = meta.Album.Id
if meta.Album.Pmid == "" {
d.albumMediaID = meta.Album.Pmid
} else {
d.albumMediaID = meta.Album.Mid
}
return nil
}
func (d *Decoder) GetCoverImage(ctx context.Context) ([]byte, error) { func (d *Decoder) GetCoverImage(ctx context.Context) ([]byte, error) {
if d.cover != nil { if d.cover != nil {
return d.cover, nil return d.cover, nil
} }
// todo: get meta if possible if d.embeddedCover {
img, err := ffmpeg.ExtractAlbumArt(ctx, d.probeBuf)
if err != nil {
return nil, fmt.Errorf("qmc[GetCoverImage] extract album art: %w", err)
}
d.cover, err = io.ReadAll(img)
if err != nil {
return nil, fmt.Errorf("qmc[GetCoverImage] read embed cover: %w", err)
}
return d.cover, nil
}
c := client.NewQQMusicClient() // todo: use global client c := client.NewQQMusicClient() // todo: use global client
var err error var err error
@ -62,4 +128,5 @@ func (d *Decoder) GetCoverImage(ctx context.Context) ([]byte, error) {
} }
return d.cover, nil return d.cover, nil
} }

View File

@ -45,6 +45,10 @@ func (r *Result) GetArtists() []string {
return artists return artists
} }
func (r *Result) HasMetadata() bool {
return r.GetTitle() != "" || r.GetAlbum() != "" || len(r.GetArtists()) > 0
}
type Format struct { type Format struct {
Filename string `json:"filename"` Filename string `json:"filename"`
NbStreams int `json:"nb_streams"` NbStreams int `json:"nb_streams"`