feat(meta): use ffmpeg to retrieve album art & metadata

This commit is contained in:
Unlock Music Dev 2022-11-24 23:28:44 +08:00
parent 9494a535a9
commit 3cf542c84c
Signed by: um-dev
GPG Key ID: 95202E10D3413A1D
2 changed files with 154 additions and 0 deletions

33
internal/ffmpeg/ffmpeg.go Normal file
View File

@ -0,0 +1,33 @@
package ffmpeg
import (
"bytes"
"context"
"fmt"
"io"
"os/exec"
)
func ExtractAlbumArt(ctx context.Context, rd io.Reader) (io.Reader, error) {
cmd := exec.CommandContext(ctx, "ffmpeg",
"-i", "pipe:0", // input from stdin
"-an", // disable audio
"-codec:v", "copy", // copy video(image) codec
"-f", "image2", // use image2 muxer
"pipe:1", // output to stdout
)
cmd.Stdin = rd
stdout, stderr := &bytes.Buffer{}, &bytes.Buffer{}
cmd.Stdout, cmd.Stderr = stdout, stderr
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("ffmpeg run: %w", err)
}
if err := cmd.Wait(); err != nil {
return nil, fmt.Errorf("ffmpeg wait: %w: %s", err, stderr.String())
}
return stdout, nil
}

121
internal/ffmpeg/ffprobe.go Normal file
View File

@ -0,0 +1,121 @@
package ffmpeg
import (
"context"
"encoding/json"
"io"
"os/exec"
"strings"
"github.com/samber/lo"
)
type Result struct {
Format *Format `json:"format"`
Streams []*Stream `json:"streams"`
}
func (r *Result) HasAttachedPic() bool {
return lo.ContainsBy(r.Streams, func(s *Stream) bool {
return s.CodecType == "video"
})
}
func (r *Result) getTagByKey(key string) string {
for k, v := range r.Format.Tags {
if key == strings.ToLower(k) {
return v
}
}
return ""
}
func (r *Result) GetTitle() string {
return r.getTagByKey("title")
}
func (r *Result) GetAlbum() string {
return r.getTagByKey("album")
}
func (r *Result) GetArtists() []string {
artists := strings.Split(r.getTagByKey("artist"), "/")
for i := range artists {
artists[i] = strings.TrimSpace(artists[i])
}
return artists
}
type Format struct {
Filename string `json:"filename"`
NbStreams int `json:"nb_streams"`
NbPrograms int `json:"nb_programs"`
FormatName string `json:"format_name"`
FormatLongName string `json:"format_long_name"`
StartTime string `json:"start_time"`
Duration string `json:"duration"`
BitRate string `json:"bit_rate"`
ProbeScore int `json:"probe_score"`
Tags map[string]string `json:"tags"`
}
type Stream struct {
Index int `json:"index"`
CodecName string `json:"codec_name"`
CodecLongName string `json:"codec_long_name"`
CodecType string `json:"codec_type"`
CodecTagString string `json:"codec_tag_string"`
CodecTag string `json:"codec_tag"`
SampleFmt string `json:"sample_fmt"`
SampleRate string `json:"sample_rate"`
Channels int `json:"channels"`
ChannelLayout string `json:"channel_layout"`
BitsPerSample int `json:"bits_per_sample"`
RFrameRate string `json:"r_frame_rate"`
AvgFrameRate string `json:"avg_frame_rate"`
TimeBase string `json:"time_base"`
StartPts int `json:"start_pts"`
StartTime string `json:"start_time"`
BitRate string `json:"bit_rate"`
Disposition *ProbeDisposition `json:"disposition"`
}
type ProbeDisposition struct {
Default int `json:"default"`
Dub int `json:"dub"`
Original int `json:"original"`
Comment int `json:"comment"`
Lyrics int `json:"lyrics"`
Karaoke int `json:"karaoke"`
Forced int `json:"forced"`
HearingImpaired int `json:"hearing_impaired"`
VisualImpaired int `json:"visual_impaired"`
CleanEffects int `json:"clean_effects"`
AttachedPic int `json:"attached_pic"`
TimedThumbnails int `json:"timed_thumbnails"`
Captions int `json:"captions"`
Descriptions int `json:"descriptions"`
Metadata int `json:"metadata"`
Dependent int `json:"dependent"`
StillImage int `json:"still_image"`
}
func ProbeReader(ctx context.Context, rd io.Reader) (*Result, error) {
cmd := exec.CommandContext(ctx, "ffprobe",
"-v", "quiet", // disable logging
"-print_format", "json", // use json format
"-show_format", "-show_streams", // retrieve format and streams
"-", // input from stdin
)
cmd.Stdin = rd
out, err := cmd.Output()
if err != nil {
return nil, err
}
ret := new(Result)
if err := json.Unmarshal(out, ret); err != nil {
return nil, err
}
return ret, nil
}