From 02be9219b1d61bd6afb61750cd3e63f471328bcb Mon Sep 17 00:00:00 2001 From: heimoshuiyu Date: Mon, 1 Jul 2024 15:17:56 +0800 Subject: [PATCH] add ffprobe tag reader for webm, mp4, mkv This is not well-tested tag reader. So FFProbe.Read() only success in following condition: - FFProbeScore == 100 --- cmd/gonic/gonic.go | 3 +- tags/ffprobe/errors.go | 9 +++ tags/ffprobe/ffprobe.go | 97 +++++++++++++++++++++++++++ tags/ffprobe/ffprobe_output_struct.go | 27 ++++++++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 tags/ffprobe/errors.go create mode 100644 tags/ffprobe/ffprobe.go create mode 100644 tags/ffprobe/ffprobe_output_struct.go diff --git a/cmd/gonic/gonic.go b/cmd/gonic/gonic.go index fe042e9..74e5d79 100644 --- a/cmd/gonic/gonic.go +++ b/cmd/gonic/gonic.go @@ -45,6 +45,7 @@ import ( "go.senan.xyz/gonic/scrobble" "go.senan.xyz/gonic/server/ctrladmin" "go.senan.xyz/gonic/server/ctrlsubsonic" + "go.senan.xyz/gonic/tags/ffprobe" "go.senan.xyz/gonic/tags/tagcommon" "go.senan.xyz/gonic/tags/taglib" "go.senan.xyz/gonic/transcode" @@ -185,7 +186,7 @@ func main() { tagReader := tagcommon.ChainReader{ taglib.TagLib{}, - // ffprobe reader? + ffprobe.FFProbe{}, // nfo reader? } diff --git a/tags/ffprobe/errors.go b/tags/ffprobe/errors.go new file mode 100644 index 0000000..4ce6882 --- /dev/null +++ b/tags/ffprobe/errors.go @@ -0,0 +1,9 @@ +package ffprobe + +import "errors" + +var ( + ErrNoMediaStreams = errors.New("no media streams") + ErrNoMediaDuration = errors.New("no media duration") + ErrFFProbeScroeNotEnough = errors.New("ffprobe score not enough") +) diff --git a/tags/ffprobe/ffprobe.go b/tags/ffprobe/ffprobe.go new file mode 100644 index 0000000..4565148 --- /dev/null +++ b/tags/ffprobe/ffprobe.go @@ -0,0 +1,97 @@ +package ffprobe + +import ( + "bytes" + "encoding/json" + "os/exec" + "path/filepath" + "strconv" + "strings" + + "go.senan.xyz/gonic/tags/tagcommon" +) + +type FFProbe struct{} + +func (FFProbe) CanRead(absPath string) bool { + switch ext := strings.ToLower(filepath.Ext(absPath)); ext { + case ".webm", ".mp4", ".mkv": + return true + } + return false +} + +func (FFProbe) Read(absPath string) (tagcommon.Info, error) { + cmd := exec.Command( + "ffprobe", + "-v", "quiet", + "-print_format", "json", + "-show_format", + "-show_streams", + ) + stdout := bytes.NewBuffer(make([]byte, 0, 1024)) + stderr := bytes.NewBuffer(make([]byte, 0, 1024)) + cmd.Stdout = stdout + cmd.Stderr = stderr + cmd.Args = append(cmd.Args, absPath) + if err := cmd.Run(); err != nil { + return nil, err + } + + var mi MediaInfo + if err := json.NewDecoder(stdout).Decode(&mi); err != nil { + return nil, err + } + + if len(mi.Streams) == 0 { + return nil, ErrNoMediaStreams + } + + if mi.Format.ProbeScore < 100 { + return nil, ErrFFProbeScroeNotEnough + } + + if mi.Format.Duration == "" { + return nil, ErrNoMediaDuration + } + + ret := &info{absPath, mi} + + return ret, nil +} + +type info struct { + abspath string + mediaInfo MediaInfo +} + +func (i *info) Title() string { return "" } +func (i *info) BrainzID() string { return "" } +func (i *info) Artist() string { return tagcommon.FallbackArtist } +func (i *info) Artists() []string { return []string{tagcommon.FallbackArtist} } +func (i *info) Album() string { return "" } +func (i *info) AlbumArtist() string { return "" } +func (i *info) AlbumArtists() []string { return []string{} } +func (i *info) AlbumBrainzID() string { return "" } +func (i *info) Genre() string { return tagcommon.FallbackGenre } +func (i *info) Genres() []string { return []string{tagcommon.FallbackGenre} } +func (i *info) TrackNumber() int { return 0 } +func (i *info) DiscNumber() int { return 0 } +func (i *info) Year() int { return 0 } + +func (i *info) ReplayGainTrackGain() float32 { return 0 } +func (i *info) ReplayGainTrackPeak() float32 { return 0 } +func (i *info) ReplayGainAlbumGain() float32 { return 0 } +func (i *info) ReplayGainAlbumPeak() float32 { return 0 } + +func (i *info) Length() int { + ret, _ := strconv.ParseFloat(i.mediaInfo.Format.Duration, 64) + return int(ret) +} + +func (i *info) Bitrate() int { + ret, _ := strconv.Atoi(i.mediaInfo.Format.BitRate) + return ret +} + +func (i *info) AbsPath() string { return i.abspath } diff --git a/tags/ffprobe/ffprobe_output_struct.go b/tags/ffprobe/ffprobe_output_struct.go new file mode 100644 index 0000000..6ef0a17 --- /dev/null +++ b/tags/ffprobe/ffprobe_output_struct.go @@ -0,0 +1,27 @@ +package ffprobe + +type Stream struct { + Index int `json:"index"` + CodecName string `json:"codec_name"` + CodecLongName string `json:"codec_long_name"` + CodecType string `json:"codec_type"` + SampleRate string `json:"sample_rate"` + Channels int `json:"channels"` + ChannelLayout string `json:"channel_layout"` +} + +type Format struct { + Filename string `json:"filename"` + NbStreams int `json:"nb_streams"` + FormatName string `json:"format_name"` + FormatLongName string `json:"format_long_name"` + Duration string `json:"duration"` + Size string `json:"size"` + BitRate string `json:"bit_rate"` + ProbeScore int `json:"probe_score"` +} + +type MediaInfo struct { + Streams []Stream `json:"streams"` + Format Format `json:"format"` +}