remove gcache dependency, switch to custom image cache with TTL for memory savings
This commit is contained in:
@@ -4,7 +4,6 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/bluele/gcache"
|
|
||||||
subsonic "github.com/dweymouth/go-subsonic"
|
subsonic "github.com/dweymouth/go-subsonic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,15 +14,12 @@ type AlbumIterator interface {
|
|||||||
type LibraryManager struct {
|
type LibraryManager struct {
|
||||||
PreCacheCoverFn func(string)
|
PreCacheCoverFn func(string)
|
||||||
|
|
||||||
s *ServerManager
|
s *ServerManager
|
||||||
albumDetailCache gcache.Cache
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLibraryManager(s *ServerManager) *LibraryManager {
|
func NewLibraryManager(s *ServerManager) *LibraryManager {
|
||||||
cache := gcache.New(250).LRU().Build()
|
|
||||||
return &LibraryManager{
|
return &LibraryManager{
|
||||||
s: s,
|
s: s,
|
||||||
albumDetailCache: cache,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,21 +81,11 @@ func (l *LibraryManager) SearchIterWithFilter(query string, filter func(*subsoni
|
|||||||
return l.newSearchIter(query, filter)
|
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) {
|
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)
|
a, err := l.s.Server.GetAlbum(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
l.albumDetailCache.Set(a.ID, a)
|
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ func StartupApp() (*App, error) {
|
|||||||
a.ServerManager = NewServerManager()
|
a.ServerManager = NewServerManager()
|
||||||
a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player)
|
a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player)
|
||||||
a.LibraryManager = NewLibraryManager(a.ServerManager)
|
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.LibraryManager.PreCacheCoverFn = func(albumID string) {
|
||||||
_, _ = a.ImageManager.GetAlbumThumbnail(albumID)
|
_, _ = a.ImageManager.GetAlbumThumbnail(albumID)
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-23
@@ -1,6 +1,7 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"container/heap"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"image"
|
"image"
|
||||||
@@ -17,7 +18,7 @@ type CacheItem struct {
|
|||||||
lastAccessed int64
|
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
|
// 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
|
// 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
|
// 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")
|
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)
|
i.cache = make(map[string]CacheItem)
|
||||||
|
go i.periodicallyEvict(ctx, evictionInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
// holds writer lock for O(i.MaxSize) worst case
|
// 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()
|
i.mu.Lock()
|
||||||
defer i.mu.Unlock()
|
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 {
|
if len(i.cache) == i.MaxSize {
|
||||||
i.evictOne()
|
i.evictOne()
|
||||||
i.cache[key] = CacheItem{
|
}
|
||||||
val: val,
|
i.cache[key] = CacheItem{
|
||||||
ttl: ttl,
|
val: val,
|
||||||
expiresAt: time.Now().Add(ttl).Unix(),
|
ttl: ttl,
|
||||||
lastAccessed: time.Now().Unix(),
|
expiresAt: time.Now().Add(ttl).Unix(),
|
||||||
}
|
lastAccessed: time.Now().Unix(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ImageCache) Add(key string, val image.Image) {
|
func (i *ImageCache) Set(key string, val image.Image) {
|
||||||
i.AddWithTTL(key, val, i.DefaultTTL)
|
i.SetWithTTL(key, val, i.DefaultTTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ImageCache) Has(key string) bool {
|
func (i *ImageCache) Has(key string) bool {
|
||||||
@@ -75,7 +77,11 @@ func (i *ImageCache) Has(key string) bool {
|
|||||||
return ok
|
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()
|
i.mu.RLock()
|
||||||
defer i.mu.RUnlock()
|
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 {
|
type expiredItem struct {
|
||||||
key string
|
key string
|
||||||
lastAccessed int64
|
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() {
|
func (i *ImageCache) EvictExpired() {
|
||||||
i.mu.RLock()
|
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()
|
now := time.Now().Unix()
|
||||||
for k, v := range i.cache {
|
for k, v := range i.cache {
|
||||||
if v.expiresAt < now {
|
if v.expiresAt < now {
|
||||||
@@ -145,16 +192,21 @@ func (i *ImageCache) EvictExpired() {
|
|||||||
}
|
}
|
||||||
i.mu.RUnlock()
|
i.mu.RUnlock()
|
||||||
|
|
||||||
count := len(i.cache)
|
heap.Init(&expired)
|
||||||
for count > i.MinSize {
|
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
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
+47
-34
@@ -1,6 +1,7 @@
|
|||||||
package backend
|
package backend
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
@@ -11,62 +12,55 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/20after4/configdir"
|
"github.com/20after4/configdir"
|
||||||
"github.com/bluele/gcache"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ImageManager struct {
|
type ImageManager struct {
|
||||||
s *ServerManager
|
s *ServerManager
|
||||||
baseCacheDir string
|
baseCacheDir string
|
||||||
thumbnailCache gcache.Cache
|
thumbnailCache ImageCache
|
||||||
|
|
||||||
cachedFullSizeCover image.Image
|
cachedFullSizeCover image.Image
|
||||||
cachedFullSizeCoverID string
|
cachedFullSizeCoverID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewImageManager(s *ServerManager, baseCacheDir string) *ImageManager {
|
func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string) *ImageManager {
|
||||||
cache := gcache.New(100).LRU().Build()
|
|
||||||
if err := configdir.MakePath(baseCacheDir); err != nil {
|
if err := configdir.MakePath(baseCacheDir); err != nil {
|
||||||
log.Println("failed to create album cover cache dir")
|
log.Println("failed to create album cover cache dir")
|
||||||
baseCacheDir = ""
|
baseCacheDir = ""
|
||||||
}
|
}
|
||||||
return &ImageManager{
|
i := &ImageManager{
|
||||||
s: s,
|
s: s,
|
||||||
baseCacheDir: baseCacheDir,
|
baseCacheDir: baseCacheDir,
|
||||||
thumbnailCache: cache,
|
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) {
|
func (i *ImageManager) GetAlbumThumbnailFromCache(albumID string) (image.Image, bool) {
|
||||||
if img, err := i.thumbnailCache.Get(albumID); err == nil && img != nil {
|
if img, err := i.thumbnailCache.GetResetTTL(albumID, true); err == nil && img != nil {
|
||||||
return img.(image.Image), true
|
return img, true
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ImageManager) GetAlbumThumbnail(albumID string) (image.Image, error) {
|
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
|
// in-memory cache
|
||||||
if i.thumbnailCache.Has(albumID) {
|
if img, err := i.thumbnailCache.GetWithNewTTL(albumID, ttl); err == nil {
|
||||||
if img, err := i.thumbnailCache.Get(albumID); err == nil {
|
return img, nil
|
||||||
return img.(image.Image), nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return i.fetchAndCacheCoverFromDiskOrServer(albumID, ttl)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *ImageManager) GetFullSizeAlbumCover(albumID string) (image.Image, error) {
|
func (i *ImageManager) GetFullSizeAlbumCover(albumID string) (image.Image, error) {
|
||||||
@@ -88,7 +82,26 @@ func (i *ImageManager) ensureCoverCacheDir() string {
|
|||||||
return path
|
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"})
|
img, err := i.s.Server.GetCoverArt(albumID, map[string]string{"size": "300"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -107,8 +120,8 @@ func (i *ImageManager) fetchAndCacheCoverFromServer(albumID string) (image.Image
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, albumID string) {
|
func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, albumID string) {
|
||||||
if time.Now().Sub(stat.ModTime()) > 24*time.Hour {
|
if time.Since(stat.ModTime()) > 24*time.Hour {
|
||||||
i.fetchAndCacheCoverFromServer(albumID)
|
i.fetchAndCacheCoverFromServer(albumID, i.thumbnailCache.DefaultTTL)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ go 1.19
|
|||||||
require (
|
require (
|
||||||
fyne.io/fyne/v2 v2.2.4
|
fyne.io/fyne/v2 v2.2.4
|
||||||
github.com/20after4/configdir v0.1.1
|
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/dweymouth/go-subsonic v0.0.0-20221214005741-bd8048fa1863
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/pelletier/go-toml v1.9.3
|
github.com/pelletier/go-toml v1.9.3
|
||||||
|
|||||||
@@ -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/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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
|
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/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/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
|
|||||||
+7
-1
@@ -8,6 +8,7 @@ import (
|
|||||||
"supersonic/ui/browsing"
|
"supersonic/ui/browsing"
|
||||||
"supersonic/ui/layouts"
|
"supersonic/ui/layouts"
|
||||||
"supersonic/ui/widgets"
|
"supersonic/ui/widgets"
|
||||||
|
"time"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
@@ -89,7 +90,12 @@ func (bp *BottomPanel) onSongChange(song *subsonic.Child) {
|
|||||||
} else {
|
} else {
|
||||||
var im image.Image
|
var im image.Image
|
||||||
if bp.ImageManager != nil {
|
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)
|
bp.NowPlaying.Update(song.Title, song.Artist, song.Album, im)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user