Compare commits

...

6 Commits

Author SHA1 Message Date
02be9219b1 add ffprobe tag reader for webm, mp4, mkv
Some checks failed
Release / Lint and test (push) Failing after 17m42s
Release / Run Release Please (push) Has been skipped
Release / Build, tag, and publish Docker image (push) Has been skipped
Release / Notify IRC (push) Has been skipped
This is not well-tested tag reader. So FFProbe.Read() only success in
following condition:
- FFProbeScore == 100
2024-07-01 15:27:00 +08:00
e8e478f2aa feat: print request duration 2024-06-25 10:43:43 +08:00
8abe131f28 mark heimoshuiyu fork version string 2024-06-25 00:34:57 +08:00
8914c59978 Use file name for unknown tag title retrieval.
When taglib get empty or null title from file/album, use file name or
folder name as the tag_title field. It could be confuse to user, but at
leaset it is better than tracks/albums can't be search in ID3 mode API.
2024-06-24 16:21:04 +08:00
332f00ff7a feat: add cli args -db-log
I add a command-line arguments "-db-log" default to false. This help me
debug with the gorm SQL.
2024-06-24 14:54:43 +08:00
71ae1029e8 fead: cli args: -podcast-download default to false
This commit turns off the podcast download ability.
Because the podcast download routine will execute a DB query every 5
sec. To temporary fix the db pressure I add this command-line flag to
turn it off. I will be looking for a way to reduce DB pressure and
manually trigger eposide downloads later on.
2024-06-24 14:46:28 +08:00
9 changed files with 176 additions and 11 deletions

View File

@@ -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"
@@ -58,6 +59,7 @@ func main() {
confPodcastPurgeAgeDays := flag.Uint("podcast-purge-age", 0, "age (in days) to purge podcast episodes if not accessed (optional)")
confPodcastPath := flag.String("podcast-path", "", "path to podcasts")
confPodcastDownload := flag.Bool("podcast-download", false, "whether to download podcasts (optional, default false)")
confCachePath := flag.String("cache-path", "", "path to cache")
@@ -67,6 +69,7 @@ func main() {
confPlaylistsPath := flag.String("playlists-path", "", "path to your list of new or existing m3u playlists that gonic can manage")
confDBPath := flag.String("db-path", "gonic.db", "path to database (optional)")
confDBLog := flag.Bool("db-log", false, "database logging (optional)")
confScanIntervalMins := flag.Uint("scan-interval", 0, "interval (in minutes) to automatically scan music (optional)")
confScanAtStart := flag.Bool("scan-at-start-enabled", false, "whether to perform an initial scan at startup (optional)")
@@ -140,6 +143,7 @@ func main() {
if err != nil {
log.Fatalf("error opening database: %v\n", err)
}
dbc.LogMode(*confDBLog)
defer dbc.Close()
err = dbc.Migrate(db.MigrationContext{
@@ -182,7 +186,7 @@ func main() {
tagReader := tagcommon.ChainReader{
taglib.TagLib{},
// ffprobe reader?
ffprobe.FFProbe{},
// nfo reader?
}
@@ -379,6 +383,10 @@ func main() {
})
errgrp.Go(func() error {
if !*confPodcastDownload {
return nil
}
defer logJob("podcast download")()
ctxTick(ctx, 5*time.Second, func() {

View File

@@ -5,6 +5,7 @@ import (
"log"
"net/http"
"strings"
"time"
)
type Middleware func(http.Handler) http.Handler
@@ -29,9 +30,15 @@ func TrimPathSuffix(suffix string) Middleware {
func Log(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
begin := time.Now()
sw := &statusWriter{ResponseWriter: w}
next.ServeHTTP(sw, r)
log.Printf("response %s %s %v", statusToBlock(sw.status), r.Method, r.URL)
elasped := time.Since(begin)
log.Printf("response %s %s %s %v",
statusToBlock(sw.status),
elasped.String(),
r.Method,
r.URL)
})
}

View File

@@ -465,8 +465,9 @@ func populateTrack(tx *db.DB, album *db.Album, track *db.Track, trags tagcommon.
track.Size = size
track.AlbumID = album.ID
track.TagTitle = trags.Title()
track.TagTitleUDec = decoded(trags.Title())
tagTitle := tagcommon.MustTitle(trags)
track.TagTitle = tagTitle
track.TagTitleUDec = decoded(tagTitle)
track.TagTrackArtist = tagcommon.MustArtist(trags)
track.TagTrackNumber = trags.TrackNumber()
track.TagDiscNumber = trags.DiscNumber()

View File

@@ -63,9 +63,9 @@
{{ slot }}
<div class="px-5 text-right whitespace-nowrap">
<span class="text-gray-500">v{{ .Version }}</span>
senan kelly, 2020
senan kelly (heimoshuiyu forked), 2020
<span class="text-gray-500">&#124;</span>
{{ component "ext_link" (props . "To" "https://github.com/sentriz/gonic") }}github{{ end }}
{{ component "ext_link" (props . "To" "https://github.com/heimoshuiyu/gonic") }}github{{ end }}
</div>
</div>
</body>

9
tags/ffprobe/errors.go Normal file
View File

@@ -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")
)

97
tags/ffprobe/ffprobe.go Normal file
View File

@@ -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 }

View File

@@ -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"`
}

View File

@@ -2,6 +2,7 @@ package tagcommon
import (
"errors"
"path"
)
var ErrUnsupported = errors.New("filetype unsupported")
@@ -33,19 +34,31 @@ type Info interface {
Length() int
Bitrate() int
AbsPath() string
}
const (
FallbackAlbum = "Unknown Album"
FallbackArtist = "Unknown Artist"
FallbackGenre = "Unknown Genre"
)
func MustTitle(p Info) string {
if r := p.Title(); r != "" {
return r
}
// return the file name for title name
return path.Base(p.AbsPath())
}
func MustAlbum(p Info) string {
if r := p.Album(); r != "" {
return r
}
return FallbackAlbum
// return the dir name for album name
return path.Base(path.Dir(p.AbsPath()))
}
func MustArtist(p Info) string {

View File

@@ -28,12 +28,13 @@ func (TagLib) Read(absPath string) (tagcommon.Info, error) {
defer f.Close()
props := f.ReadAudioProperties()
raw := f.ReadTags()
return &info{raw, props}, nil
return &info{raw, props, absPath}, nil
}
type info struct {
raw map[string][]string
props *audiotags.AudioProperties
raw map[string][]string
props *audiotags.AudioProperties
abspath string
}
// https://picard-docs.musicbrainz.org/downloads/MusicBrainz_Picard_Tag_Map.html
@@ -60,6 +61,8 @@ func (i *info) ReplayGainAlbumPeak() float32 { return flt(first(find(i.raw, "rep
func (i *info) Length() int { return i.props.Length }
func (i *info) Bitrate() int { return i.props.Bitrate }
func (i *info) AbsPath() string { return i.abspath }
func first[T comparable](is []T) T {
var z T
for _, i := range is {