Fix #227: add limit to on-disk image cache size, periodic pruning
This commit is contained in:
@@ -103,6 +103,8 @@ func StartupApp(appName, displayAppName, appVersionTag, configFile, latestReleas
|
||||
a.ServerManager = NewServerManager(appName, a.Config)
|
||||
a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling)
|
||||
a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(a.appName))
|
||||
a.Config.Application.MaxImageCacheSizeMB = clamp(a.Config.Application.MaxImageCacheSizeMB, 1, 500)
|
||||
a.ImageManager.SetMaxOnDiskCacheSizeBytes(int64(a.Config.Application.MaxImageCacheSizeMB) * 1_048_576)
|
||||
a.ServerManager.SetPrefetchAlbumCoverCallback(func(coverID string) {
|
||||
_, _ = a.ImageManager.GetCoverThumbnail(coverID)
|
||||
})
|
||||
|
||||
+18
-16
@@ -22,14 +22,15 @@ type ServerConfig struct {
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
WindowWidth int
|
||||
WindowHeight int
|
||||
LastCheckedVersion string
|
||||
EnableSystemTray bool
|
||||
CloseToSystemTray bool
|
||||
StartupPage string
|
||||
SettingsTab string
|
||||
AllowMultiInstance bool
|
||||
WindowWidth int
|
||||
WindowHeight int
|
||||
LastCheckedVersion string
|
||||
EnableSystemTray bool
|
||||
CloseToSystemTray bool
|
||||
StartupPage string
|
||||
SettingsTab string
|
||||
AllowMultiInstance bool
|
||||
MaxImageCacheSizeMB int
|
||||
|
||||
// Experimental - may be removed in future
|
||||
FontNormalTTF string
|
||||
@@ -119,14 +120,15 @@ var SupportedStartupPages = []string{"Albums", "Favorites", "Playlists"}
|
||||
func DefaultConfig(appVersionTag string) *Config {
|
||||
return &Config{
|
||||
Application: AppConfig{
|
||||
WindowWidth: 1000,
|
||||
WindowHeight: 800,
|
||||
LastCheckedVersion: appVersionTag,
|
||||
EnableSystemTray: true,
|
||||
CloseToSystemTray: false,
|
||||
StartupPage: "Albums",
|
||||
SettingsTab: "General",
|
||||
AllowMultiInstance: false,
|
||||
WindowWidth: 1000,
|
||||
WindowHeight: 800,
|
||||
LastCheckedVersion: appVersionTag,
|
||||
EnableSystemTray: true,
|
||||
CloseToSystemTray: false,
|
||||
StartupPage: "Albums",
|
||||
SettingsTab: "General",
|
||||
AllowMultiInstance: false,
|
||||
MaxImageCacheSizeMB: 50,
|
||||
},
|
||||
AlbumPage: AlbumPageConfig{
|
||||
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite", "Rating"},
|
||||
|
||||
+65
-1
@@ -7,10 +7,13 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
@@ -23,8 +26,13 @@ const CachedImageValidTime = 24 * time.Hour
|
||||
const (
|
||||
coverArtThumbnailSize = 300
|
||||
fullSizeCoverExpires = 5 * time.Minute
|
||||
|
||||
defaultDiskCacheSizeBytes = 50 * 1_048_576
|
||||
)
|
||||
|
||||
// The ImageManager is responsible for retrieving and serving images to the UI layer.
|
||||
// It maintains an in-memory cache of recently used images for immediate future access,
|
||||
// and a larger on-disc cache of images that is periodically re-requested from the server.
|
||||
type ImageManager struct {
|
||||
s *ServerManager
|
||||
baseCacheDir string
|
||||
@@ -33,6 +41,9 @@ type ImageManager struct {
|
||||
cachedFullSizeCover image.Image
|
||||
cachedFullSizeCoverID string
|
||||
cachedFullSizeCoverAccessedAt int64 // unixMillis
|
||||
|
||||
maxOnDiskCacheSizeBytes int64
|
||||
filesWrittenSinceLastPrune bool
|
||||
}
|
||||
|
||||
func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string) *ImageManager {
|
||||
@@ -48,16 +59,24 @@ func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string)
|
||||
MaxSize: 150,
|
||||
DefaultTTL: 1 * time.Minute,
|
||||
},
|
||||
maxOnDiskCacheSizeBytes: defaultDiskCacheSizeBytes,
|
||||
}
|
||||
s.OnLogout(func() {
|
||||
i.thumbnailCache.Clear()
|
||||
i.clearFullSizeCover()
|
||||
})
|
||||
i.thumbnailCache.OnEvictTaskRan = i.clearFullSizeCoverIfExpired
|
||||
i.thumbnailCache.OnEvictTaskRan = func() {
|
||||
i.clearFullSizeCoverIfExpired()
|
||||
i.pruneOnDiskCache()
|
||||
}
|
||||
i.thumbnailCache.Init(ctx, 2*time.Minute)
|
||||
return i
|
||||
}
|
||||
|
||||
func (i *ImageManager) SetMaxOnDiskCacheSizeBytes(size int64) {
|
||||
i.maxOnDiskCacheSizeBytes = size
|
||||
}
|
||||
|
||||
func (i *ImageManager) GetCoverThumbnailFromCache(coverID string) (image.Image, bool) {
|
||||
img, err := i.thumbnailCache.GetExtendTTL(coverID, i.thumbnailCache.DefaultTTL)
|
||||
if err == nil && img != nil {
|
||||
@@ -214,6 +233,7 @@ func (i *ImageManager) writeJpeg(img image.Image, path string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
i.filesWrittenSinceLastPrune = true
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -238,3 +258,47 @@ func (i *ImageManager) clearFullSizeCover() {
|
||||
i.cachedFullSizeCoverID = ""
|
||||
i.cachedFullSizeCover = nil
|
||||
}
|
||||
|
||||
func (im *ImageManager) pruneOnDiskCache() {
|
||||
if !im.filesWrittenSinceLastPrune {
|
||||
return // no new covers cached since last run, no need to walk dir
|
||||
}
|
||||
|
||||
// collect list of all cached covers (across servers)
|
||||
// we use modTime as a proxy for last accessed time
|
||||
// since covers are refreshed from the server after a fixed interval,
|
||||
// modTime is roughly equivalent to last access
|
||||
type fileInfo struct {
|
||||
path string
|
||||
size int64
|
||||
modTime int64
|
||||
}
|
||||
var allCovers []fileInfo
|
||||
var totalSize int64
|
||||
filepath.WalkDir(im.baseCacheDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() || !strings.HasSuffix(path, "jpg") {
|
||||
return nil
|
||||
}
|
||||
if info, err := d.Info(); err == nil {
|
||||
s := info.Size()
|
||||
allCovers = append(allCovers,
|
||||
fileInfo{path: path, size: s, modTime: info.ModTime().UnixMilli()})
|
||||
totalSize += s
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if totalSize > im.maxOnDiskCacheSizeBytes {
|
||||
// sort and then delete from least recently modified until size is under threshold
|
||||
sort.Slice(allCovers, func(i, j int) bool {
|
||||
return allCovers[i].modTime < allCovers[j].modTime
|
||||
})
|
||||
for i := 0; i < len(allCovers) && totalSize > im.maxOnDiskCacheSizeBytes; i++ {
|
||||
if err := os.Remove(allCovers[i].path); err == nil {
|
||||
totalSize -= allCovers[i].size
|
||||
}
|
||||
}
|
||||
}
|
||||
im.filesWrittenSinceLastPrune = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user