Feat: 获取&写入 音频文件的 Metadata #43
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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"`
|
||||||
|
Loading…
Reference in New Issue
Block a user