From ab5612eba44c6b60dbb401568026e3445463a757 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 9 Jan 2023 17:17:44 -0800 Subject: [PATCH 1/5] beginning work on custom image cache --- backend/imagecache.go | 118 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 backend/imagecache.go diff --git a/backend/imagecache.go b/backend/imagecache.go new file mode 100644 index 0000000..8875416 --- /dev/null +++ b/backend/imagecache.go @@ -0,0 +1,118 @@ +package backend + +import ( + "context" + "errors" + "image" + "sync" + "time" +) + +type CacheItem struct { + val image.Image + ttl time.Duration + + // unix time + expiresAt int64 + lastAccessed int64 +} + +// A custom cache for images with the following eviction strategy: +// - If there are fewer than MinSize items in the cache, none will be evicted +// - If a new addition would make the cache exceed MaxSize, an item will be immediately evicted +// - evicted item will be an expired item if possible, else the least recently used +// - If the size of the cache is between MaxSize and MinSize, expired items will be periodically evicted +type ImageCache struct { + MinSize int + MaxSize int + DefaultTTL time.Duration + + mu sync.RWMutex + cache map[string]CacheItem +} + +var ( + ErrNotFound = errors.New("item not found") +) + +func (i *ImageCache) Init(ctx context.Context) { + i.cache = make(map[string]CacheItem) +} + +func (i *ImageCache) AddWithTTL(key string, val image.Image, ttl time.Duration) { + i.mu.Lock() + defer i.mu.Unlock() + + if v, ok := i.cache[key]; ok { + v.val = val + v.ttl = ttl + v.expiresAt = time.Now().Add(v.ttl).Unix() + v.lastAccessed = time.Now().Unix() + return + } + if len(i.cache) == i.MaxSize { + i.evictOne() + i.cache[key] = CacheItem{ + val: val, + ttl: ttl, + expiresAt: time.Now().Add(ttl).Unix(), + lastAccessed: time.Now().Unix(), + } + } +} + +func (i *ImageCache) Add(key string, val image.Image) { + i.AddWithTTL(key, val, i.DefaultTTL) +} + +func (i *ImageCache) Has(key string) bool { + i.mu.RLock() + defer i.mu.RUnlock() + + _, ok := i.cache[key] + return ok +} + +func (i *ImageCache) Get(key string, resetTTL bool) (image.Image, error) { + i.mu.RLock() + defer i.mu.RUnlock() + + if v, ok := i.cache[key]; ok { + v.lastAccessed = time.Now().Unix() + if resetTTL { + v.expiresAt = time.Now().Add(v.ttl).Unix() + } + return v.val, nil + } + return nil, ErrNotFound +} + +// assuming max size is small enough that linear scan is fine +func (i *ImageCache) evictOne() { + var lruKey string + now := time.Now().Unix() + lruTime := now + for k, v := range i.cache { + if v.expiresAt < now { + delete(i.cache, k) + return + } + if v.lastAccessed < lruTime { + lruTime = v.lastAccessed + lruKey = k + } + } + delete(i.cache, lruKey) +} + +// TODO +func (i *ImageCache) EvictExpired() { + i.mu.Lock() + defer i.mu.Unlock() + + count := len(i.cache) + for count > i.MinSize { + + } + +} From 0c06e13423c1468eaa399385c1ad679fd022e802 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 16 Jan 2023 16:58:43 -0800 Subject: [PATCH 2/5] more work on image cache --- backend/imagecache.go | 66 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 12 deletions(-) diff --git a/backend/imagecache.go b/backend/imagecache.go index 8875416..45160cf 100644 --- a/backend/imagecache.go +++ b/backend/imagecache.go @@ -18,10 +18,11 @@ type CacheItem struct { } // A custom cache for images with the following eviction strategy: -// - If there are fewer than MinSize items in the cache, none will be evicted -// - If a new addition would make the cache exceed MaxSize, an item will be immediately evicted -// - evicted item will be an expired item if possible, else the least recently used -// - If the size of the cache is between MaxSize and MinSize, expired items will be periodically evicted +// 1. If there are fewer than MinSize items in the cache, none will be evicted +// 2. If a new addition would make the cache exceed MaxSize, an item will be immediately evicted +// 2a. in this case, evict the LRU expired item or if none expired, the LRU item +// 3. If the size of the cache is between MaxSize and MinSize, expired items will be periodically evicted +// 3a. in this case, again the least recently used expired items will be evicted first type ImageCache struct { MinSize int MaxSize int @@ -39,6 +40,7 @@ func (i *ImageCache) Init(ctx context.Context) { i.cache = make(map[string]CacheItem) } +// holds writer lock for O(i.MaxSize) worst case func (i *ImageCache) AddWithTTL(key string, val image.Image, ttl time.Duration) { i.mu.Lock() defer i.mu.Unlock() @@ -87,28 +89,61 @@ func (i *ImageCache) Get(key string, resetTTL bool) (image.Image, error) { return nil, ErrNotFound } -// assuming max size is small enough that linear scan is fine +func (i *ImageCache) GetWithNewTTL(key string, newTtl time.Duration) (image.Image, error) { + i.mu.RLock() + defer i.mu.RUnlock() + + if v, ok := i.cache[key]; ok { + v.lastAccessed = time.Now().Unix() + v.expiresAt = time.Now().Add(newTtl).Unix() + v.ttl = newTtl + return v.val, nil + } + return nil, ErrNotFound +} + +// must be called when rwmutex is already acquired for writing func (i *ImageCache) evictOne() { - var lruKey string now := time.Now().Unix() + var lruKey string lruTime := now + var lruExpiredKey string + lruExpiredTime := now for k, v := range i.cache { - if v.expiresAt < now { - delete(i.cache, k) - return + if v.expiresAt < now && v.lastAccessed < lruExpiredTime { + lruExpiredTime = v.lastAccessed + lruExpiredKey = k } if v.lastAccessed < lruTime { lruTime = v.lastAccessed lruKey = k } } - delete(i.cache, lruKey) + if lruExpiredTime < now { + // deleting LRU expired item + delete(i.cache, lruExpiredKey) + } else { + // no expired items, delete LRU non-expired item + delete(i.cache, lruKey) + } +} + +type expiredItem struct { + key string + lastAccessed int64 } // TODO func (i *ImageCache) EvictExpired() { - i.mu.Lock() - defer i.mu.Unlock() + i.mu.RLock() + expired := make([]expiredItem, 0, len(i.cache)-i.MinSize) + now := time.Now().Unix() + for k, v := range i.cache { + if v.expiresAt < now { + expired = append(expired, expiredItem{key: k, lastAccessed: v.lastAccessed}) + } + } + i.mu.RUnlock() count := len(i.cache) for count > i.MinSize { @@ -116,3 +151,10 @@ func (i *ImageCache) EvictExpired() { } } + +func heapify(arr *[]expiredItem, i int) { + //smallest := i + //lChild := 2*i + 1 + //rChild := 2*i + 2 + +} From 867461dab56691c5b480193f4de7354f9936c1cb Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 2 Feb 2023 15:52:59 -0800 Subject: [PATCH 3/5] remove gcache dependency, switch to custom image cache with TTL for memory savings --- backend/albumiterator.go | 18 +------- backend/app.go | 2 +- backend/imagecache.go | 98 ++++++++++++++++++++++++++++++---------- backend/imagemanager.go | 81 +++++++++++++++++++-------------- go.mod | 1 - go.sum | 2 - ui/bottompanel.go | 8 +++- 7 files changed, 132 insertions(+), 78 deletions(-) diff --git a/backend/albumiterator.go b/backend/albumiterator.go index 6ef2c76..9b163f3 100644 --- a/backend/albumiterator.go +++ b/backend/albumiterator.go @@ -4,7 +4,6 @@ import ( "log" "strconv" - "github.com/bluele/gcache" subsonic "github.com/dweymouth/go-subsonic" ) @@ -15,15 +14,12 @@ type AlbumIterator interface { type LibraryManager struct { PreCacheCoverFn func(string) - s *ServerManager - albumDetailCache gcache.Cache + s *ServerManager } func NewLibraryManager(s *ServerManager) *LibraryManager { - cache := gcache.New(250).LRU().Build() return &LibraryManager{ - s: s, - albumDetailCache: cache, + s: s, } } @@ -85,21 +81,11 @@ func (l *LibraryManager) SearchIterWithFilter(query string, filter func(*subsoni return l.newSearchIter(query, filter) } -func (l *LibraryManager) CacheAlbum(a *subsonic.AlbumID3) { - l.albumDetailCache.Set(a.ID, a) -} - func (l *LibraryManager) GetAlbum(id string) (*subsonic.AlbumID3, error) { - if l.albumDetailCache.Has(id) { - if a, err := l.albumDetailCache.Get(id); err == nil { - return a.(*subsonic.AlbumID3), nil - } - } a, err := l.s.Server.GetAlbum(id) if err != nil { return nil, err } - l.albumDetailCache.Set(a.ID, a) return a, nil } diff --git a/backend/app.go b/backend/app.go index 4d3713e..285d7c7 100644 --- a/backend/app.go +++ b/backend/app.go @@ -50,7 +50,7 @@ func StartupApp() (*App, error) { a.ServerManager = NewServerManager() a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player) a.LibraryManager = NewLibraryManager(a.ServerManager) - a.ImageManager = NewImageManager(a.ServerManager, configdir.LocalCache(AppName)) + a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(AppName)) a.LibraryManager.PreCacheCoverFn = func(albumID string) { _, _ = a.ImageManager.GetAlbumThumbnail(albumID) } diff --git a/backend/imagecache.go b/backend/imagecache.go index 45160cf..23f117f 100644 --- a/backend/imagecache.go +++ b/backend/imagecache.go @@ -1,6 +1,7 @@ package backend import ( + "container/heap" "context" "errors" "image" @@ -17,7 +18,7 @@ type CacheItem struct { lastAccessed int64 } -// A custom cache for images with the following eviction strategy: +// A custom in-memory cache for images with the following eviction strategy: // 1. If there are fewer than MinSize items in the cache, none will be evicted // 2. If a new addition would make the cache exceed MaxSize, an item will be immediately evicted // 2a. in this case, evict the LRU expired item or if none expired, the LRU item @@ -36,12 +37,13 @@ var ( ErrNotFound = errors.New("item not found") ) -func (i *ImageCache) Init(ctx context.Context) { +func (i *ImageCache) Init(ctx context.Context, evictionInterval time.Duration) { i.cache = make(map[string]CacheItem) + go i.periodicallyEvict(ctx, evictionInterval) } // holds writer lock for O(i.MaxSize) worst case -func (i *ImageCache) AddWithTTL(key string, val image.Image, ttl time.Duration) { +func (i *ImageCache) SetWithTTL(key string, val image.Image, ttl time.Duration) { i.mu.Lock() defer i.mu.Unlock() @@ -54,17 +56,17 @@ func (i *ImageCache) AddWithTTL(key string, val image.Image, ttl time.Duration) } if len(i.cache) == i.MaxSize { i.evictOne() - i.cache[key] = CacheItem{ - val: val, - ttl: ttl, - expiresAt: time.Now().Add(ttl).Unix(), - lastAccessed: time.Now().Unix(), - } + } + i.cache[key] = CacheItem{ + val: val, + ttl: ttl, + expiresAt: time.Now().Add(ttl).Unix(), + lastAccessed: time.Now().Unix(), } } -func (i *ImageCache) Add(key string, val image.Image) { - i.AddWithTTL(key, val, i.DefaultTTL) +func (i *ImageCache) Set(key string, val image.Image) { + i.SetWithTTL(key, val, i.DefaultTTL) } func (i *ImageCache) Has(key string) bool { @@ -75,7 +77,11 @@ func (i *ImageCache) Has(key string) bool { return ok } -func (i *ImageCache) Get(key string, resetTTL bool) (image.Image, error) { +func (i *ImageCache) Get(key string) (image.Image, error) { + return i.GetResetTTL(key, false) +} + +func (i *ImageCache) GetResetTTL(key string, resetTTL bool) (image.Image, error) { i.mu.RLock() defer i.mu.RUnlock() @@ -128,15 +134,56 @@ func (i *ImageCache) evictOne() { } } +func (i *ImageCache) periodicallyEvict(ctx context.Context, interval time.Duration) { + t := time.NewTicker(interval) + for { + select { + case <-ctx.Done(): + t.Stop() + return + case <-t.C: + i.EvictExpired() + } + } +} + type expiredItem struct { key string lastAccessed int64 } -// TODO +type expiredHeap []expiredItem + +func (h expiredHeap) Len() int { return len(h) } +func (h expiredHeap) Less(i, j int) bool { return h[i].lastAccessed < h[j].lastAccessed } +func (h expiredHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *expiredHeap) Push(x any) { + // Push and Pop use pointer receivers because they modify the slice's length, + // not just its contents. + *h = append(*h, x.(expiredItem)) +} + +func (h *expiredHeap) Pop() any { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + return x +} + +// EvictExpired evicts least recently used expired items from the cache +// until there are no more expired items or the cache contains MinSize elements +// Holds the reader lock for O(n) time and writer lock for O(n) func (i *ImageCache) EvictExpired() { i.mu.RLock() - expired := make([]expiredItem, 0, len(i.cache)-i.MinSize) + count := len(i.cache) + sliceCap := count - i.MinSize + if sliceCap <= 0 { + i.mu.RUnlock() + return + } + expired := make(expiredHeap, 0, sliceCap) now := time.Now().Unix() for k, v := range i.cache { if v.expiresAt < now { @@ -145,16 +192,21 @@ func (i *ImageCache) EvictExpired() { } i.mu.RUnlock() - count := len(i.cache) - for count > i.MinSize { + heap.Init(&expired) + var keysToRemove []string + for count > i.MinSize && len(expired) > 0 { + keysToRemove = append(keysToRemove, heap.Pop(&expired).(expiredItem).key) + count -= 1 + } + i.mu.Lock() + defer i.mu.Unlock() + for _, key := range keysToRemove { + // during the interim when we don't hold the lock, some expired items + // could have been re-set, so check expiry again + if item, ok := i.cache[key]; ok && item.expiresAt < now { + delete(i.cache, key) + } } } - -func heapify(arr *[]expiredItem, i int) { - //smallest := i - //lChild := 2*i + 1 - //rChild := 2*i + 2 - -} diff --git a/backend/imagemanager.go b/backend/imagemanager.go index 3858efe..2f9d068 100644 --- a/backend/imagemanager.go +++ b/backend/imagemanager.go @@ -1,6 +1,7 @@ package backend import ( + "context" "fmt" "image" "image/jpeg" @@ -11,62 +12,55 @@ import ( "time" "github.com/20after4/configdir" - "github.com/bluele/gcache" ) type ImageManager struct { s *ServerManager baseCacheDir string - thumbnailCache gcache.Cache + thumbnailCache ImageCache cachedFullSizeCover image.Image cachedFullSizeCoverID string } -func NewImageManager(s *ServerManager, baseCacheDir string) *ImageManager { - cache := gcache.New(100).LRU().Build() +func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string) *ImageManager { if err := configdir.MakePath(baseCacheDir); err != nil { log.Println("failed to create album cover cache dir") baseCacheDir = "" } - return &ImageManager{ - s: s, - baseCacheDir: baseCacheDir, - thumbnailCache: cache, + i := &ImageManager{ + s: s, + baseCacheDir: baseCacheDir, + thumbnailCache: ImageCache{ + MinSize: 24, + MaxSize: 150, + DefaultTTL: 1 * time.Minute, + }, } + i.thumbnailCache.Init(ctx, 2*time.Minute) + return i } func (i *ImageManager) GetAlbumThumbnailFromCache(albumID string) (image.Image, bool) { - if img, err := i.thumbnailCache.Get(albumID); err == nil && img != nil { - return img.(image.Image), true + if img, err := i.thumbnailCache.GetResetTTL(albumID, true); err == nil && img != nil { + return img, true } return nil, false } func (i *ImageManager) GetAlbumThumbnail(albumID string) (image.Image, error) { + if im, ok := i.GetAlbumThumbnailFromCache(albumID); ok { + return im, nil + } + return i.fetchAndCacheCoverFromDiskOrServer(albumID, i.thumbnailCache.DefaultTTL) +} + +func (i *ImageManager) GetAlbumThumbnailWithTTL(albumID string, ttl time.Duration) (image.Image, error) { // in-memory cache - if i.thumbnailCache.Has(albumID) { - if img, err := i.thumbnailCache.Get(albumID); err == nil { - return img.(image.Image), nil - } + if img, err := i.thumbnailCache.GetWithNewTTL(albumID, ttl); err == nil { + return img, nil } - - // on disc cache - path := i.filePathForCover(albumID) - if i.ensureCoverCacheDir() != "" { - if s, err := os.Stat(path); err == nil { - go i.checkRefreshLocalCover(s, albumID) - if f, err := os.Open(path); err == nil { - defer f.Close() - if img, _, err := image.Decode(f); err == nil { - i.thumbnailCache.Set(albumID, img) - return img, nil - } - } - } - } - - return i.fetchAndCacheCoverFromServer(albumID) + return i.fetchAndCacheCoverFromDiskOrServer(albumID, ttl) } func (i *ImageManager) GetFullSizeAlbumCover(albumID string) (image.Image, error) { @@ -88,7 +82,26 @@ func (i *ImageManager) ensureCoverCacheDir() string { return path } -func (i *ImageManager) fetchAndCacheCoverFromServer(albumID string) (image.Image, error) { +func (i *ImageManager) fetchAndCacheCoverFromDiskOrServer(albumID string, ttl time.Duration) (image.Image, error) { + // on disc cache + path := i.filePathForCover(albumID) + if i.ensureCoverCacheDir() != "" { + if s, err := os.Stat(path); err == nil { + go i.checkRefreshLocalCover(s, albumID) + if f, err := os.Open(path); err == nil { + defer f.Close() + if img, _, err := image.Decode(f); err == nil { + i.thumbnailCache.SetWithTTL(albumID, img, ttl) + return img, nil + } + } + } + } + + return i.fetchAndCacheCoverFromServer(albumID, ttl) +} + +func (i *ImageManager) fetchAndCacheCoverFromServer(albumID string, ttl time.Duration) (image.Image, error) { img, err := i.s.Server.GetCoverArt(albumID, map[string]string{"size": "300"}) if err != nil { return nil, err @@ -107,8 +120,8 @@ func (i *ImageManager) fetchAndCacheCoverFromServer(albumID string) (image.Image } func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, albumID string) { - if time.Now().Sub(stat.ModTime()) > 24*time.Hour { - i.fetchAndCacheCoverFromServer(albumID) + if time.Since(stat.ModTime()) > 24*time.Hour { + i.fetchAndCacheCoverFromServer(albumID, i.thumbnailCache.DefaultTTL) } } diff --git a/go.mod b/go.mod index 8d5a62d..67534a0 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.19 require ( fyne.io/fyne/v2 v2.2.4 github.com/20after4/configdir v0.1.1 - github.com/bluele/gcache v0.0.2 github.com/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863 github.com/google/uuid v1.3.0 github.com/pelletier/go-toml v1.9.3 diff --git a/go.sum b/go.sum index d3de868..9145ccc 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,6 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= -github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= diff --git a/ui/bottompanel.go b/ui/bottompanel.go index c6d0ec8..f1f430d 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -8,6 +8,7 @@ import ( "supersonic/ui/browsing" "supersonic/ui/layouts" "supersonic/ui/widgets" + "time" "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" @@ -89,7 +90,12 @@ func (bp *BottomPanel) onSongChange(song *subsonic.Child) { } else { var im image.Image if bp.ImageManager != nil { - im, _ = bp.ImageManager.GetAlbumThumbnail(song.AlbumID) + // set image to expire not long after the length of the song + // if song is played through without much pausing, image will still + // be in cache for the next song if it's from the same album, or + // if the user navigates to the album page for the track + imgTTLSec := song.Duration + 30 + im, _ = bp.ImageManager.GetAlbumThumbnailWithTTL(song.AlbumID, time.Duration(imgTTLSec)*time.Second) } bp.NowPlaying.Update(song.Title, song.Artist, song.Album, im) } From 261726a5c6377a2d83c5de51db51797646b448df Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 2 Feb 2023 16:15:29 -0800 Subject: [PATCH 4/5] add GetExtendTTL --- backend/imagecache.go | 14 ++++++++++++++ backend/imagemanager.go | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/imagecache.go b/backend/imagecache.go index 23f117f..299ede6 100644 --- a/backend/imagecache.go +++ b/backend/imagecache.go @@ -95,6 +95,20 @@ func (i *ImageCache) GetResetTTL(key string, resetTTL bool) (image.Image, error) return nil, ErrNotFound } +// Gets the image if it exists and extends TTL to time.Now + ttl iff the image would expire before then +func (i *ImageCache) GetExtendTTL(key string, ttl time.Duration) (image.Image, error) { + i.mu.RLock() + defer i.mu.RUnlock() + if v, ok := i.cache[key]; ok { + v.lastAccessed = time.Now().Unix() + if v.expiresAt < time.Now().Add(ttl).Unix() { + v.expiresAt = time.Now().Add(ttl).Unix() + } + return v.val, nil + } + return nil, ErrNotFound +} + func (i *ImageCache) GetWithNewTTL(key string, newTtl time.Duration) (image.Image, error) { i.mu.RLock() defer i.mu.RUnlock() diff --git a/backend/imagemanager.go b/backend/imagemanager.go index 2f9d068..9a2a8ac 100644 --- a/backend/imagemanager.go +++ b/backend/imagemanager.go @@ -42,7 +42,8 @@ func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string) } func (i *ImageManager) GetAlbumThumbnailFromCache(albumID string) (image.Image, bool) { - if img, err := i.thumbnailCache.GetResetTTL(albumID, true); err == nil && img != nil { + img, err := i.thumbnailCache.GetExtendTTL(albumID, i.thumbnailCache.DefaultTTL) + if err == nil && img != nil { return img, true } return nil, false From 6c4a74027d26f2013ff58356305ee0a044607c8e Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 2 Feb 2023 16:25:33 -0800 Subject: [PATCH 5/5] pass TTL through --- backend/imagemanager.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/imagemanager.go b/backend/imagemanager.go index 9a2a8ac..b281e1a 100644 --- a/backend/imagemanager.go +++ b/backend/imagemanager.go @@ -88,7 +88,7 @@ func (i *ImageManager) fetchAndCacheCoverFromDiskOrServer(albumID string, ttl ti path := i.filePathForCover(albumID) if i.ensureCoverCacheDir() != "" { if s, err := os.Stat(path); err == nil { - go i.checkRefreshLocalCover(s, albumID) + go i.checkRefreshLocalCover(s, albumID, ttl) if f, err := os.Open(path); err == nil { defer f.Close() if img, _, err := image.Decode(f); err == nil { @@ -116,13 +116,13 @@ func (i *ImageManager) fetchAndCacheCoverFromServer(albumID string, ttl time.Dur } } } - i.thumbnailCache.Set(albumID, img) + i.thumbnailCache.SetWithTTL(albumID, img, ttl) return img, nil } -func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, albumID string) { +func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, albumID string, ttl time.Duration) { if time.Since(stat.ModTime()) > 24*time.Hour { - i.fetchAndCacheCoverFromServer(albumID, i.thumbnailCache.DefaultTTL) + i.fetchAndCacheCoverFromServer(albumID, ttl) } }