limit concurrent image fetches from server
This commit is contained in:
+79
-22
@@ -21,13 +21,13 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const CachedImageValidTime = 24 * time.Hour
|
||||
|
||||
const (
|
||||
coverArtThumbnailSize = 300
|
||||
cachedImageValidTime = 24 * time.Hour
|
||||
fullSizeCoverExpires = 5 * time.Minute
|
||||
|
||||
defaultDiskCacheSizeBytes = 50 * 1_048_576
|
||||
maxConcurrentServerFetches = 5
|
||||
defaultDiskCacheSizeBytes = 50 * 1_048_576
|
||||
)
|
||||
|
||||
// The ImageManager is responsible for retrieving and serving images to the UI layer.
|
||||
@@ -44,8 +44,11 @@ type ImageManager struct {
|
||||
|
||||
maxOnDiskCacheSizeBytes int64
|
||||
filesWrittenSinceLastPrune bool
|
||||
|
||||
serverFetchSema chan interface{}
|
||||
}
|
||||
|
||||
// NewImageManager returns a new ImageManager.
|
||||
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")
|
||||
@@ -60,6 +63,7 @@ func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string)
|
||||
DefaultTTL: 1 * time.Minute,
|
||||
},
|
||||
maxOnDiskCacheSizeBytes: defaultDiskCacheSizeBytes,
|
||||
serverFetchSema: make(chan interface{}, maxConcurrentServerFetches),
|
||||
}
|
||||
s.OnLogout(func() {
|
||||
i.thumbnailCache.Clear()
|
||||
@@ -73,10 +77,14 @@ func NewImageManager(ctx context.Context, s *ServerManager, baseCacheDir string)
|
||||
return i
|
||||
}
|
||||
|
||||
// SetMaxOnDiskCacheSizeBytes sets the maximum size of the on-disc cover thumbnail cache.
|
||||
// A periodic clean task will delete least recently accessed images to maintain the size limit.
|
||||
func (i *ImageManager) SetMaxOnDiskCacheSizeBytes(size int64) {
|
||||
i.maxOnDiskCacheSizeBytes = size
|
||||
}
|
||||
|
||||
// GetCoverThumbnailFromCache returns the cover thumbnail for the given ID if it exists
|
||||
// in the in-memory cache. Returns quickly, safe to call in UI threads.
|
||||
func (i *ImageManager) GetCoverThumbnailFromCache(coverID string) (image.Image, bool) {
|
||||
img, err := i.thumbnailCache.GetExtendTTL(coverID, i.thumbnailCache.DefaultTTL)
|
||||
if err == nil && img != nil {
|
||||
@@ -85,21 +93,44 @@ func (i *ImageManager) GetCoverThumbnailFromCache(coverID string) (image.Image,
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// GetCoverThumbnailAsync asynchronously fetches the cover image for the given ID,
|
||||
// and invokes the callback on completion. It returns a context.CancelFunc which can be used to
|
||||
// cancel the fetch. The callback will not be invoked if the fetch is cancelled before completion.
|
||||
// The cancel func must be invoked to avoid resource leaks. Use GetCoverThumbnail if cancellation is not needed.
|
||||
func (i *ImageManager) GetCoverThumbnailAsync(coverID string, cb func(image.Image, error)) context.CancelFunc {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
if im, ok := i.GetCoverThumbnailFromCache(coverID); ok {
|
||||
if ctx.Err() == nil {
|
||||
cb(im, nil)
|
||||
}
|
||||
} else {
|
||||
i.fetchAndCacheCoverFromDiskOrServer(ctx, coverID, i.thumbnailCache.DefaultTTL, cb)
|
||||
}
|
||||
}()
|
||||
return cancel
|
||||
}
|
||||
|
||||
// GetCoverThumbnail is a synchronous, blocking function to fetch the image for a given coverID.
|
||||
// Like most ImageManager calls, it should usually be called in a goroutine to not block UI loading.
|
||||
func (i *ImageManager) GetCoverThumbnail(coverID string) (image.Image, error) {
|
||||
if im, ok := i.GetCoverThumbnailFromCache(coverID); ok {
|
||||
return im, nil
|
||||
}
|
||||
return i.fetchAndCacheCoverFromDiskOrServer(coverID, i.thumbnailCache.DefaultTTL)
|
||||
return i.fetchAndCacheCoverFromDiskOrServer(context.Background(), coverID, i.thumbnailCache.DefaultTTL, nil)
|
||||
}
|
||||
|
||||
// GetCoverThumbnailWithTTL fetches the cover for the given coverID and updates the TTL
|
||||
// in the in-memory image cache. It blocks until the image fetch is complete.
|
||||
func (i *ImageManager) GetCoverThumbnailWithTTL(coverID string, ttl time.Duration) (image.Image, error) {
|
||||
// in-memory cache
|
||||
if img, err := i.thumbnailCache.GetWithNewTTL(coverID, ttl); err == nil {
|
||||
return img, nil
|
||||
}
|
||||
return i.fetchAndCacheCoverFromDiskOrServer(coverID, ttl)
|
||||
return i.fetchAndCacheCoverFromDiskOrServer(context.Background(), coverID, ttl, nil)
|
||||
}
|
||||
|
||||
// GetFullSizeCoverArt fetches the full size cover image for the given coverID.
|
||||
// It blocks until the fetch is complete.
|
||||
func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error) {
|
||||
if i.cachedFullSizeCoverID == coverID {
|
||||
i.cachedFullSizeCoverAccessedAt = time.Now().UnixMilli()
|
||||
@@ -108,7 +139,10 @@ func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error)
|
||||
if i.s.Server == nil {
|
||||
return nil, errors.New("logged out")
|
||||
}
|
||||
|
||||
i.serverFetchSema <- struct{}{} // acquire
|
||||
im, err := i.s.Server.GetCoverArt(coverID, 0)
|
||||
<-i.serverFetchSema // release
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -118,6 +152,7 @@ func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error)
|
||||
return im, nil
|
||||
}
|
||||
|
||||
// GetCoverArtURL returns the URL for the locally cached cover thumbnail, if it exists.
|
||||
func (i *ImageManager) GetCoverArtUrl(coverID string) (string, error) {
|
||||
path := i.filePathForCover(coverID)
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
@@ -128,10 +163,13 @@ func (i *ImageManager) GetCoverArtUrl(coverID string) (string, error) {
|
||||
return "", errors.New("cover not found")
|
||||
}
|
||||
|
||||
// GetCachedArtistImage returns the artist image for the given artistID from the on-disc cache, if it exists.
|
||||
func (i *ImageManager) GetCachedArtistImage(artistID string) (image.Image, bool) {
|
||||
return i.loadLocalImage(i.filePathForArtistImage(artistID))
|
||||
}
|
||||
|
||||
// FetchAndCacheArtistImage fetches the artist image for the given artistID from the server,
|
||||
// caching it locally if the fetch succeeds. Blocks until fetch is completed.
|
||||
func (i *ImageManager) FetchAndCacheArtistImage(artistID string, imgURL string) (image.Image, error) {
|
||||
im, err := i.fetchRemoteArtistImage(imgURL)
|
||||
if err != nil {
|
||||
@@ -141,9 +179,10 @@ func (i *ImageManager) FetchAndCacheArtistImage(artistID string, imgURL string)
|
||||
return im, nil
|
||||
}
|
||||
|
||||
// RefreshCachedArtistImageIfExpired re-fetches the artist image from the server if expired.
|
||||
func (i *ImageManager) RefreshCachedArtistImageIfExpired(artistID string, imgURL string) error {
|
||||
stat, err := os.Stat(i.filePathForArtistImage(artistID))
|
||||
if err == nil && time.Since(stat.ModTime()) > CachedImageValidTime {
|
||||
if err == nil && time.Since(stat.ModTime()) > cachedImageValidTime {
|
||||
_, err = i.FetchAndCacheArtistImage(artistID, imgURL)
|
||||
}
|
||||
return err
|
||||
@@ -167,7 +206,10 @@ func (i *ImageManager) ensureArtistCoverCacheDir() string {
|
||||
}
|
||||
|
||||
func (i *ImageManager) fetchRemoteArtistImage(url string) (image.Image, error) {
|
||||
i.serverFetchSema <- struct{}{} // acquire
|
||||
res, err := fyne.LoadResourceFromURLString(url)
|
||||
<-i.serverFetchSema // release
|
||||
|
||||
if err == nil {
|
||||
im, _, err := image.Decode(bytes.NewReader(res.Content()))
|
||||
if err == nil {
|
||||
@@ -178,41 +220,56 @@ func (i *ImageManager) fetchRemoteArtistImage(url string) (image.Image, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (i *ImageManager) fetchAndCacheCoverFromDiskOrServer(coverID string, ttl time.Duration) (image.Image, error) {
|
||||
func (i *ImageManager) fetchAndCacheCoverFromDiskOrServer(ctx context.Context, coverID string, ttl time.Duration, cb func(image.Image, error)) (image.Image, error) {
|
||||
// on disc cache
|
||||
path := i.filePathForCover(coverID)
|
||||
if i.ensureCoverCacheDir() != "" {
|
||||
path := i.filePathForCover(coverID)
|
||||
if s, err := os.Stat(path); err == nil {
|
||||
go i.checkRefreshLocalCover(s, coverID, ttl)
|
||||
if img, ok := i.loadLocalImage(path); ok {
|
||||
i.thumbnailCache.SetWithTTL(coverID, img, ttl)
|
||||
if ctx.Err() == nil && cb != nil {
|
||||
cb(img, nil)
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return i.fetchAndCacheCoverFromServer(coverID, ttl)
|
||||
// fetch from server
|
||||
return i.fetchAndCacheCoverFromServer(ctx, coverID, ttl, cb)
|
||||
}
|
||||
|
||||
func (i *ImageManager) fetchAndCacheCoverFromServer(coverID string, ttl time.Duration) (image.Image, error) {
|
||||
func (i *ImageManager) fetchAndCacheCoverFromServer(ctx context.Context, coverID string, ttl time.Duration, cb func(image.Image, error)) (image.Image, error) {
|
||||
if i.s.Server == nil {
|
||||
return nil, errors.New("logged out")
|
||||
}
|
||||
img, err := i.s.Server.GetCoverArt(coverID, coverArtThumbnailSize)
|
||||
if err != nil {
|
||||
err := errors.New("logged out")
|
||||
if ctx.Err() == nil && cb != nil {
|
||||
cb(nil, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if i.ensureCoverCacheDir() != "" {
|
||||
path := i.filePathForCover(coverID)
|
||||
_ = i.writeJpeg(img, path)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, context.Canceled
|
||||
case i.serverFetchSema <- struct{}{}: // acquire
|
||||
img, err := i.s.Server.GetCoverArt(coverID, coverArtThumbnailSize)
|
||||
<-i.serverFetchSema // release
|
||||
if err == nil {
|
||||
if i.ensureCoverCacheDir() != "" {
|
||||
_ = i.writeJpeg(img, i.filePathForCover(coverID))
|
||||
}
|
||||
i.thumbnailCache.SetWithTTL(coverID, img, ttl)
|
||||
}
|
||||
if ctx.Err() == nil && cb != nil {
|
||||
cb(img, err)
|
||||
}
|
||||
return img, err
|
||||
}
|
||||
i.thumbnailCache.SetWithTTL(coverID, img, ttl)
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, coverID string, ttl time.Duration) {
|
||||
if time.Since(stat.ModTime()) > CachedImageValidTime {
|
||||
i.fetchAndCacheCoverFromServer(coverID, ttl)
|
||||
if time.Since(stat.ModTime()) > cachedImageValidTime {
|
||||
i.fetchAndCacheCoverFromServer(context.Background(), coverID, ttl, nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user