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