feat(meta): use ffmpeg to retrieve album art & metadata
parent
9494a535a9
commit
3cf542c84c
@ -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
|
||||
}
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue