From 3cf542c84c882ab457456a3979a242fe8f7d442a Mon Sep 17 00:00:00 2001 From: Unlock Music Dev Date: Thu, 24 Nov 2022 23:28:44 +0800 Subject: [PATCH] feat(meta): use ffmpeg to retrieve album art & metadata --- internal/ffmpeg/ffmpeg.go | 33 ++++++++++ internal/ffmpeg/ffprobe.go | 121 +++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 internal/ffmpeg/ffmpeg.go create mode 100644 internal/ffmpeg/ffprobe.go diff --git a/internal/ffmpeg/ffmpeg.go b/internal/ffmpeg/ffmpeg.go new file mode 100644 index 0000000..2487244 --- /dev/null +++ b/internal/ffmpeg/ffmpeg.go @@ -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 +} diff --git a/internal/ffmpeg/ffprobe.go b/internal/ffmpeg/ffprobe.go new file mode 100644 index 0000000..53a3d00 --- /dev/null +++ b/internal/ffmpeg/ffprobe.go @@ -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 +}