use user selected profile for transcoding

This commit is contained in:
sentriz
2020-03-12 00:27:49 +00:00
parent 885d7702be
commit eec0b0bf1f
5 changed files with 79 additions and 65 deletions

View File

@@ -57,6 +57,7 @@ then start with `docker-compose up -d`
|env var|command line arg|description| |env var|command line arg|description|
|---|---|---| |---|---|---|
|`GONIC_MUSIC_PATH`|`-music-path`|path to your music collection| |`GONIC_MUSIC_PATH`|`-music-path`|path to your music collection|
|`GONIC_CACHE_PATH`|`-cache-path`|**optional** path to store audio transcodes (*default* `/tmp/gonic_cache`)|
|`GONIC_DB_PATH`|`-db-path`|**optional** path to database file| |`GONIC_DB_PATH`|`-db-path`|**optional** path to database file|
|`GONIC_LISTEN_ADDR`|`-listen-addr`|**optional** host and port to listen on (eg. `0.0.0.0:4747`, `127.0.0.1:4747`) (*default* `0.0.0.0:4747`)| |`GONIC_LISTEN_ADDR`|`-listen-addr`|**optional** host and port to listen on (eg. `0.0.0.0:4747`, `127.0.0.1:4747`) (*default* `0.0.0.0:4747`)|
|`GONIC_PROXY_PREFIX`|`-proxy-prefix`|**optional** url path prefix to use if behind reverse proxy. eg `/gonic` (see example configs below)| |`GONIC_PROXY_PREFIX`|`-proxy-prefix`|**optional** url path prefix to use if behind reverse proxy. eg `/gonic` (see example configs below)|

View File

@@ -62,7 +62,7 @@ var Bytes = map[string]*EmbeddedAsset{
0x7b,0x20,0x65,0x6e,0x64,0x20,0x7d,0x7d,0x0a, 0x7b,0x20,0x65,0x6e,0x64,0x20,0x7d,0x7d,0x0a,
}}, }},
"pages/home.tmpl": &EmbeddedAsset{ "pages/home.tmpl": &EmbeddedAsset{
ModTime: time.Unix(1583954752, 0), ModTime: time.Unix(1583972849, 0),
Bytes: []byte{ Bytes: []byte{
0x7b,0x7b,0x20,0x64,0x65,0x66,0x69,0x6e,0x65,0x20,0x22,0x75,0x73,0x65,0x72,0x22,0x20,0x7d,0x7d,0x0a,0x3c,0x64,0x69,0x76, 0x7b,0x7b,0x20,0x64,0x65,0x66,0x69,0x6e,0x65,0x20,0x22,0x75,0x73,0x65,0x72,0x22,0x20,0x7d,0x7d,0x0a,0x3c,0x64,0x69,0x76,
0x20,0x63,0x6c,0x61,0x73,0x73,0x3d,0x22,0x70,0x61,0x64,0x64,0x65,0x64,0x20,0x62,0x6f,0x78,0x22,0x3e,0x0a,0x20,0x20,0x20, 0x20,0x63,0x6c,0x61,0x73,0x73,0x3d,0x22,0x70,0x61,0x64,0x64,0x65,0x64,0x20,0x62,0x6f,0x78,0x22,0x3e,0x0a,0x20,0x20,0x20,

View File

@@ -83,6 +83,17 @@ func (t *Track) MIME() string {
return mime.Types[ext] return mime.Types[ext]
} }
func (t *Track) RelPath() string {
if t.Album == nil {
return ""
}
return path.Join(
t.Album.LeftPath,
t.Album.RightPath,
t.Filename,
)
}
type User struct { type User struct {
ID int `gorm:"primary_key"` ID int `gorm:"primary_key"`
CreatedAt time.Time CreatedAt time.Time

View File

@@ -10,32 +10,11 @@ import (
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"senan.xyz/g/gonic/db" "senan.xyz/g/gonic/db"
"senan.xyz/g/gonic/mime"
"senan.xyz/g/gonic/server/ctrlsubsonic/params" "senan.xyz/g/gonic/server/ctrlsubsonic/params"
"senan.xyz/g/gonic/server/ctrlsubsonic/spec" "senan.xyz/g/gonic/server/ctrlsubsonic/spec"
"senan.xyz/g/gonic/server/encode" "senan.xyz/g/gonic/server/encode"
) )
// Put special clients that can't handle Opus here:
func encodeProfileFor(client string) string {
switch client {
case "Soundwaves":
return "mp3_rg"
case "Jamstash":
return "opus_rg"
default:
return "opus"
}
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
// "raw" handlers are ones that don't always return a spec response. // "raw" handlers are ones that don't always return a spec response.
// it could be a file, stream, etc. so you must either // it could be a file, stream, etc. so you must either
// a) write to response writer // a) write to response writer
@@ -69,6 +48,49 @@ func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *s
return nil return nil
} }
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return false
}
return !info.IsDir()
}
type serveTrackOptions struct {
track *db.Track
pref *db.TranscodePreference
maxBitrate int
cachePath string
musicPath string
}
func serveTrackRaw(w http.ResponseWriter, r *http.Request, opts serveTrackOptions) {
log.Printf("serving raw %q\n", opts.track.Filename)
w.Header().Set("Content-Type", opts.track.MIME())
trackPath := path.Join(opts.musicPath, opts.track.RelPath())
http.ServeFile(w, r, trackPath)
}
func serveTrackEncode(w http.ResponseWriter, r *http.Request, opts serveTrackOptions) {
profile := encode.Profiles[opts.pref.Profile]
bitrate := encode.GetBitrate(opts.maxBitrate, profile)
trackPath := path.Join(opts.musicPath, opts.track.RelPath())
cacheKey := encode.CacheKey(trackPath, opts.pref.Profile, bitrate)
cacheFile := path.Join(opts.cachePath, cacheKey)
if fileExists(cacheFile) {
log.Printf("serving transcode `%s`: cache [%s/%s] hit!\n", opts.track.Filename, profile.Format, bitrate)
http.ServeFile(w, r, cacheFile)
return
}
log.Printf("serving transcode `%s`: cache [%s/%s] miss!\n", opts.track.Filename, profile.Format, bitrate)
if err := encode.Encode(w, trackPath, cacheFile, profile, bitrate); err != nil {
log.Printf("error encoding %q: %v\n", trackPath, err)
return
}
log.Printf("serving transcode `%s`: encoded to [%s/%s] successfully\n",
opts.track.Filename, profile.Format, bitrate)
}
func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.Response { func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.Response {
params := r.Context().Value(CtxParams).(params.Params) params := r.Context().Value(CtxParams).(params.Params)
id, err := params.GetInt("id") id, err := params.GetInt("id")
@@ -83,8 +105,8 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
if gorm.IsRecordNotFoundError(err) { if gorm.IsRecordNotFoundError(err) {
return spec.NewError(70, "media with id `%d` was not found", id) return spec.NewError(70, "media with id `%d` was not found", id)
} }
user := r.Context().Value(CtxUser).(*db.User)
defer func() { defer func() {
user := r.Context().Value(CtxUser).(*db.User)
play := db.Play{ play := db.Play{
AlbumID: track.Album.ID, AlbumID: track.Album.ID,
UserID: user.ID, UserID: user.ID,
@@ -96,34 +118,24 @@ func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.R
play.Count++ // for getAlbumList?type=frequent play.Count++ // for getAlbumList?type=frequent
c.DB.Save(&play) c.DB.Save(&play)
}() }()
client := params.GetOr("c", "generic") client := params.GetOr("c", "*")
maxBitrate, err := params.GetInt("maxBitRate") servOpts := serveTrackOptions{
if err != nil { track: track,
maxBitrate = 0 musicPath: c.MusicPath,
} }
pref := &db.TranscodePreference{}
absPath := path.Join( err = c.DB.
c.MusicPath, Where("user_id=? AND client=? COLLATE NOCASE", user.ID, client).
track.Album.LeftPath, First(pref).
track.Album.RightPath, Error
track.Filename, if gorm.IsRecordNotFoundError(err) {
) serveTrackRaw(w, r, servOpts)
profileName := encodeProfileFor(client)
profile := encode.Profiles[profileName]
bitrate := encode.GetBitrate(maxBitrate, profile)
cacheKey := encode.CacheKey(absPath, profileName, bitrate)
cacheFile := path.Join(c.CachePath, cacheKey)
if fileExists(cacheFile) {
log.Printf("track `%s`: cache [%s/%s] hit!\n", track.Filename, profile.Format, bitrate)
http.ServeFile(w, r, cacheFile)
return nil return nil
} }
log.Printf("track `%s`: cache [%s/%s] miss!\n", track.Filename, profile.Format, bitrate) servOpts.pref = pref
if err := encode.Encode(w, absPath, cacheFile, profile, bitrate); err != nil { servOpts.maxBitrate = params.GetIntOr("maxBitRate", 0)
log.Printf("error encoding %q: %v\n", absPath, err) servOpts.cachePath = c.CachePath
} serveTrackEncode(w, r, servOpts)
log.Printf("track `%s`: encoded to [%s/%s] successfully\n",
track.Filename, profile.Format, bitrate)
return nil return nil
} }
@@ -141,20 +153,9 @@ func (c *Controller) ServeDownload(w http.ResponseWriter, r *http.Request) *spec
if gorm.IsRecordNotFoundError(err) { if gorm.IsRecordNotFoundError(err) {
return spec.NewError(70, "media with id `%d` was not found", id) return spec.NewError(70, "media with id `%d` was not found", id)
} }
serveTrackRaw(w, r, serveTrackOptions{
absPath := path.Join( track: track,
c.MusicPath, musicPath: c.MusicPath,
track.Album.LeftPath, })
track.Album.RightPath,
track.Filename,
)
if mime, ok := mime.Types[track.Ext()]; ok {
w.Header().Set("Content-Type", mime)
}
http.ServeFile(w, r, absPath)
//
// We don't need to mark album/track as played
// if user just downloads a track, so bail out here:
return nil return nil
} }

View File

@@ -132,9 +132,10 @@ func Encode(out io.Writer, trackPath, cachePath string, profile *Profile, bitrat
} }
// Generate cache key (file name). For, you know, encoded tracks cache. // Generate cache key (file name). For, you know, encoded tracks cache.
func CacheKey(sourcePath string, profile string, bitrate string) string { func CacheKey(sourcePath string, profile, bitrate string) string {
format := Profiles[profile].Format format := Profiles[profile].Format
return fmt.Sprintf("%x-%s-%s.%s", xxhash.Sum64String(sourcePath), profile, bitrate, format) hash := xxhash.Sum64String(sourcePath)
return fmt.Sprintf("%x-%s-%s.%s", hash, profile, bitrate, format)
} }
// Check if client forces bitrate lower than set in profile: // Check if client forces bitrate lower than set in profile: