From 112d9ab28e49856653f999fae1ae8b3c5ffded5b Mon Sep 17 00:00:00 2001 From: Unlock Music Dev Date: Fri, 25 Nov 2022 04:29:59 +0800 Subject: [PATCH] feat(qmc): allow retrieve metadata online --- algo/qmc/qmc.go | 11 ++++-- algo/qmc/qmc_meta.go | 73 ++++++++++++++++++++++++++++++++++++-- internal/ffmpeg/ffprobe.go | 4 +++ 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/algo/qmc/qmc.go b/algo/qmc/qmc.go index 7e8597f..0ea6c81 100644 --- a/algo/qmc/qmc.go +++ b/algo/qmc/qmc.go @@ -34,8 +34,10 @@ type Decoder struct { albumMediaID string // cache - meta common.AudioMeta - cover []byte + meta common.AudioMeta + 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 logger *zap.Logger @@ -48,6 +50,8 @@ func (d *Decoder) Read(p []byte) (int, error) { if n > 0 { d.cipher.Decrypt(p[:n], d.offset) d.offset += n + + _, _ = d.probeBuf.Write(p[:n]) // bytes.Buffer.Write never return error } return n, err } @@ -89,6 +93,9 @@ func (d *Decoder) Validate() error { } d.audio = io.LimitReader(d.raw, int64(d.audioLen)) + // prepare for sniffing metadata + d.probeBuf = bytes.NewBuffer(make([]byte, 0, d.audioLen)) + return nil } diff --git a/algo/qmc/qmc_meta.go b/algo/qmc/qmc_meta.go index 23493be..6a93b32 100644 --- a/algo/qmc/qmc_meta.go +++ b/algo/qmc/qmc_meta.go @@ -4,9 +4,14 @@ import ( "context" "errors" "fmt" + "io" + "strings" + + "github.com/samber/lo" "unlock-music.dev/cli/algo/common" "unlock-music.dev/cli/algo/qmc/client" + "unlock-music.dev/cli/internal/ffmpeg" ) 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 { - 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 { @@ -38,12 +60,56 @@ func (d *Decoder) getMetaBySongID(ctx context.Context) error { 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) { if 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 var err error @@ -62,4 +128,5 @@ func (d *Decoder) GetCoverImage(ctx context.Context) ([]byte, error) { } return d.cover, nil + } diff --git a/internal/ffmpeg/ffprobe.go b/internal/ffmpeg/ffprobe.go index 53a3d00..f98ef9d 100644 --- a/internal/ffmpeg/ffprobe.go +++ b/internal/ffmpeg/ffprobe.go @@ -45,6 +45,10 @@ func (r *Result) GetArtists() []string { return artists } +func (r *Result) HasMetadata() bool { + return r.GetTitle() != "" || r.GetAlbum() != "" || len(r.GetArtists()) > 0 +} + type Format struct { Filename string `json:"filename"` NbStreams int `json:"nb_streams"`