diff --git a/README.md b/README.md index af2a117b..6cafaca4 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ password can then be changed from the web interface | `GONIC_MULTI_VALUE_GENRE` | `-multi-value-genre` | **optional** setting for multi-valued genre tags when scanning ([see more](#multi-valued-tags-v016)) | | `GONIC_MULTI_VALUE_ARTIST` | `-multi-value-artist` | **optional** setting for multi-valued artist tags when scanning ([see more](#multi-valued-tags-v016)) | | `GONIC_MULTI_VALUE_ALBUM_ARTIST` | `-multi-value-album-artist` | **optional** setting for multi-valued album artist tags when scanning ([see more](#multi-valued-tags-v016)) | +| `GONIC_TRANSCODE_CACHE_SIZE` | `-transcode-cache-size` | **optional** size of the transcode cache in MB (0 = no limit) | +| `GONIC_TRANSCODE_EJECT_INTERVAL` | `-transcode-eject-interval` | **optional** interval (in minutes) to eject transcode cache (0 = never) | | `GONIC_EXPVAR` | `-expvar` | **optional** enable the /debug/vars endpoint (exposes useful debugging attributes as well as database stats) | ## multi valued tags (v0.16+) diff --git a/cmd/gonic/gonic b/cmd/gonic/gonic new file mode 100755 index 00000000..a78947de Binary files /dev/null and b/cmd/gonic/gonic differ diff --git a/cmd/gonic/gonic.go b/cmd/gonic/gonic.go index 4548904c..e7e99bcb 100644 --- a/cmd/gonic/gonic.go +++ b/cmd/gonic/gonic.go @@ -93,6 +93,9 @@ func main() { deprecatedConfGenreSplit := flag.String("genre-split", "", "(deprecated, see multi-value settings)") + confTranscodeCacheSize := flag.Int("transcode-cache-size", 0, "size of the transcode cache in MB (0 = no limit) (optional)") + confTranscodeEjectInterval := flag.Int("transcode-eject-interval", 0, "interval (in minutes) to eject transcode cache (0 = never) (optional)") + flag.Parse() flagconf.ParseEnv() flagconf.ParseConfig(*confConfigPath) @@ -201,6 +204,7 @@ func main() { transcoder := transcode.NewCachingTranscoder( transcode.NewFFmpegTranscoder(), cacheDirAudio, + *confTranscodeCacheSize, ) lastfmClientKeySecretFunc := func() (string, string, error) { @@ -404,6 +408,19 @@ func main() { return nil }) + errgrp.Go(func() error { + if *confTranscodeEjectInterval == 0 || *confTranscodeCacheSize == 0 { + return nil + } + + defer logJob("transcode cache eject")() + + ctxTick(ctx, time.Duration(*confTranscodeEjectInterval)*time.Minute, func() { + transcoder.CacheEject() + }) + return nil + }) + errgrp.Go(func() error { if *confScanIntervalMins == 0 { return nil diff --git a/transcode/transcode_test.go b/transcode/transcode_test.go index eecced28..b28b7b6e 100644 --- a/transcode/transcode_test.go +++ b/transcode/transcode_test.go @@ -113,7 +113,7 @@ func TestCachingParallelism(t *testing.T) { callback: func() { realTranscodeCount.Add(1) }, } - cacheTranscoder := transcode.NewCachingTranscoder(transcoder, t.TempDir()) + cacheTranscoder := transcode.NewCachingTranscoder(transcoder, t.TempDir(), 1024) var wg sync.WaitGroup for i := 0; i < 5; i++ { diff --git a/transcode/transcoder_caching.go b/transcode/transcoder_caching.go index d402652e..b66a618c 100644 --- a/transcode/transcoder_caching.go +++ b/transcode/transcoder_caching.go @@ -7,7 +7,9 @@ import ( "io" "os" "path/filepath" + "sort" "sync" + "time" ) const perm = 0o644 @@ -15,13 +17,14 @@ const perm = 0o644 type CachingTranscoder struct { cachePath string transcoder Transcoder + limitMb int locks keyedMutex } var _ Transcoder = (*CachingTranscoder)(nil) -func NewCachingTranscoder(t Transcoder, cachePath string) *CachingTranscoder { - return &CachingTranscoder{transcoder: t, cachePath: cachePath} +func NewCachingTranscoder(t Transcoder, cachePath string, limitMb int) *CachingTranscoder { + return &CachingTranscoder{transcoder: t, cachePath: cachePath, limitMb: limitMb} } func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in string, out io.Writer) error { @@ -52,6 +55,7 @@ func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in s if i, err := cf.Stat(); err == nil && i.Size() > 0 { _, _ = io.Copy(out, cf) + _ = os.Chtimes(path, time.Now(), time.Now()) // Touch for LRU cache purposes return nil } @@ -64,6 +68,41 @@ func (t *CachingTranscoder) Transcode(ctx context.Context, profile Profile, in s return nil } +func (t *CachingTranscoder) CacheEject() { + // Delete LRU cache files that exceed size limit. Use last modified time. + type file struct { + path string + info os.FileInfo + } + + var files []file + var total int64 = 0 + + _ = filepath.Walk(t.cachePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + files = append(files, file{path, info}) + total += info.Size() + } + return nil + }) + + sort.Slice(files, func(i, j int) bool { + return files[i].info.ModTime().Before(files[j].info.ModTime()) + }) + + for total > int64(t.limitMb)*1024*1024 { + curFile := files[0] + files = files[1:] + total -= curFile.info.Size() + unlock := t.locks.Lock(curFile.path) + _ = os.Remove(curFile.path) + unlock() + } +} + func cacheKey(cmd string, args []string) string { // the cache is invalid whenever transcode command (which includes the // absolute filepath, bit rate args, replay gain args, etc.) changes