Compare commits
6 Commits
b70979d2e0
...
02be9219b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
02be9219b1
|
|||
|
e8e478f2aa
|
|||
|
8abe131f28
|
|||
|
8914c59978
|
|||
|
332f00ff7a
|
|||
|
71ae1029e8
|
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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">|</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
9
tags/ffprobe/errors.go
Normal 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
97
tags/ffprobe/ffprobe.go
Normal 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 }
|
||||
27
tags/ffprobe/ffprobe_output_struct.go
Normal file
27
tags/ffprobe/ffprobe_output_struct.go
Normal 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"`
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user