diff --git a/README.md b/README.md index f8800e3..fc42604 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ then start with `docker-compose up -d` |env var|command line arg|description| |---|---|---| |`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_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)| diff --git a/assets/assets_gen.go b/assets/assets_gen.go index 48bb662..6d9cb26 100644 --- a/assets/assets_gen.go +++ b/assets/assets_gen.go @@ -62,7 +62,7 @@ var Bytes = map[string]*EmbeddedAsset{ 0x7b,0x20,0x65,0x6e,0x64,0x20,0x7d,0x7d,0x0a, }}, "pages/home.tmpl": &EmbeddedAsset{ - ModTime: time.Unix(1583954752, 0), + ModTime: time.Unix(1583972849, 0), 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, 0x20,0x63,0x6c,0x61,0x73,0x73,0x3d,0x22,0x70,0x61,0x64,0x64,0x65,0x64,0x20,0x62,0x6f,0x78,0x22,0x3e,0x0a,0x20,0x20,0x20, diff --git a/db/model.go b/db/model.go index 7bbcaa9..73754bd 100644 --- a/db/model.go +++ b/db/model.go @@ -94,6 +94,17 @@ func (t *Track) MIME() string { 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 { ID int `gorm:"primary_key"` CreatedAt time.Time diff --git a/server/ctrlsubsonic/handlers_raw.go b/server/ctrlsubsonic/handlers_raw.go index 14807d9..ff5e9aa 100644 --- a/server/ctrlsubsonic/handlers_raw.go +++ b/server/ctrlsubsonic/handlers_raw.go @@ -10,32 +10,11 @@ import ( "github.com/jinzhu/gorm" "senan.xyz/g/gonic/db" - "senan.xyz/g/gonic/mime" "senan.xyz/g/gonic/server/ctrlsubsonic/params" "senan.xyz/g/gonic/server/ctrlsubsonic/spec" "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. // it could be a file, stream, etc. so you must either // a) write to response writer @@ -69,6 +48,49 @@ func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *s 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 { params := r.Context().Value(CtxParams).(params.Params) 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) { return spec.NewError(70, "media with id `%d` was not found", id) } + user := r.Context().Value(CtxUser).(*db.User) defer func() { - user := r.Context().Value(CtxUser).(*db.User) play := db.Play{ AlbumID: track.Album.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 c.DB.Save(&play) }() - client := params.GetOr("c", "generic") - maxBitrate, err := params.GetInt("maxBitRate") - if err != nil { - maxBitrate = 0 + client := params.GetOr("c", "*") + servOpts := serveTrackOptions{ + track: track, + musicPath: c.MusicPath, } - - absPath := path.Join( - c.MusicPath, - track.Album.LeftPath, - track.Album.RightPath, - track.Filename, - ) - 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) + pref := &db.TranscodePreference{} + err = c.DB. + Where("user_id=? AND client=? COLLATE NOCASE", user.ID, client). + First(pref). + Error + if gorm.IsRecordNotFoundError(err) { + serveTrackRaw(w, r, servOpts) return nil } - log.Printf("track `%s`: cache [%s/%s] miss!\n", track.Filename, profile.Format, bitrate) - if err := encode.Encode(w, absPath, cacheFile, profile, bitrate); err != nil { - log.Printf("error encoding %q: %v\n", absPath, err) - } - log.Printf("track `%s`: encoded to [%s/%s] successfully\n", - track.Filename, profile.Format, bitrate) + servOpts.pref = pref + servOpts.maxBitrate = params.GetIntOr("maxBitRate", 0) + servOpts.cachePath = c.CachePath + serveTrackEncode(w, r, servOpts) return nil } @@ -141,20 +153,9 @@ func (c *Controller) ServeDownload(w http.ResponseWriter, r *http.Request) *spec if gorm.IsRecordNotFoundError(err) { return spec.NewError(70, "media with id `%d` was not found", id) } - - absPath := path.Join( - 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: + serveTrackRaw(w, r, serveTrackOptions{ + track: track, + musicPath: c.MusicPath, + }) return nil } diff --git a/server/encode/encode.go b/server/encode/encode.go index 985bad0..d89e45e 100644 --- a/server/encode/encode.go +++ b/server/encode/encode.go @@ -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. -func CacheKey(sourcePath string, profile string, bitrate string) string { +func CacheKey(sourcePath string, profile, bitrate string) string { 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: