add caching of artist images

This commit is contained in:
Drew Weymouth
2023-03-19 11:01:20 -07:00
parent 005191c9c3
commit 2380caab35
4 changed files with 95 additions and 23 deletions
+3
View File
@@ -1,5 +1,8 @@
# Change Log # Change Log
## Unreleased
- [#39](https://github.com/dweymouth/supersonic/issues/39) Add caching of artist images
## [0.0.1-alpha2] - 2023-03-17 ## [0.0.1-alpha2] - 2023-03-17
### Added ### Added
+72 -11
View File
@@ -1,6 +1,7 @@
package backend package backend
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"image" "image"
@@ -11,9 +12,12 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"fyne.io/fyne/v2"
"github.com/20after4/configdir" "github.com/20after4/configdir"
) )
const CachedImageValidTime = 24 * time.Hour
type ImageManager struct { type ImageManager struct {
s *ServerManager s *ServerManager
baseCacheDir string baseCacheDir string
@@ -77,27 +81,63 @@ func (i *ImageManager) GetFullSizeAlbumCover(albumID string) (image.Image, error
return im, nil return im, nil
} }
func (i *ImageManager) GetCachedArtistImage(artistID string) (image.Image, bool) {
return i.loadLocalImage(i.filePathForArtistImage(artistID))
}
func (i *ImageManager) FetchAndCacheArtistImage(artistID string, imgURL string) (image.Image, error) {
im, err := i.fetchRemoteArtistImage(imgURL)
if err != nil {
return nil, err
}
_ = i.writeJpeg(im, i.filePathForArtistImage(artistID))
return im, nil
}
func (i *ImageManager) RefreshCachedArtistImageIfExpired(artistID string, imgURL string) error {
stat, err := os.Stat(i.filePathForArtistImage(artistID))
if err == nil && time.Since(stat.ModTime()) > CachedImageValidTime {
_, err = i.FetchAndCacheArtistImage(artistID, imgURL)
}
return err
}
func (i *ImageManager) ensureCoverCacheDir() string { func (i *ImageManager) ensureCoverCacheDir() string {
path := path.Join(i.baseCacheDir, i.s.ServerID.String(), "covers") path := path.Join(i.baseCacheDir, i.s.ServerID.String(), "covers")
configdir.MakePath(path) configdir.MakePath(path)
return path return path
} }
func (i *ImageManager) ensureArtistCoverCacheDir() string {
path := path.Join(i.baseCacheDir, i.s.ServerID.String(), "artistimages")
configdir.MakePath(path)
return path
}
func (i *ImageManager) fetchRemoteArtistImage(url string) (image.Image, error) {
res, err := fyne.LoadResourceFromURLString(url)
if err == nil {
im, _, err := image.Decode(bytes.NewReader(res.Content()))
if err == nil {
return im, nil
}
return nil, err
}
return nil, err
}
func (i *ImageManager) fetchAndCacheCoverFromDiskOrServer(albumID string, ttl time.Duration) (image.Image, error) { func (i *ImageManager) fetchAndCacheCoverFromDiskOrServer(albumID string, ttl time.Duration) (image.Image, error) {
// on disc cache // on disc cache
path := i.filePathForCover(albumID) path := i.filePathForCover(albumID)
if i.ensureCoverCacheDir() != "" { if i.ensureCoverCacheDir() != "" {
if s, err := os.Stat(path); err == nil { if s, err := os.Stat(path); err == nil {
go i.checkRefreshLocalCover(s, albumID, ttl) go i.checkRefreshLocalCover(s, albumID, ttl)
if f, err := os.Open(path); err == nil { if img, ok := i.loadLocalImage(path); ok {
defer f.Close()
if img, _, err := image.Decode(f); err == nil {
i.thumbnailCache.SetWithTTL(albumID, img, ttl) i.thumbnailCache.SetWithTTL(albumID, img, ttl)
return img, nil return img, nil
} }
} }
} }
}
return i.fetchAndCacheCoverFromServer(albumID, ttl) return i.fetchAndCacheCoverFromServer(albumID, ttl)
} }
@@ -109,19 +149,14 @@ func (i *ImageManager) fetchAndCacheCoverFromServer(albumID string, ttl time.Dur
} }
if i.ensureCoverCacheDir() != "" { if i.ensureCoverCacheDir() != "" {
path := i.filePathForCover(albumID) path := i.filePathForCover(albumID)
if f, err := os.Create(path); err == nil { _ = i.writeJpeg(img, path)
defer f.Close()
if err := jpeg.Encode(f, img, nil /*options*/); err != nil {
log.Printf("failed to cache image: %s", err.Error())
}
}
} }
i.thumbnailCache.SetWithTTL(albumID, img, ttl) i.thumbnailCache.SetWithTTL(albumID, img, ttl)
return img, nil return img, nil
} }
func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, albumID string, ttl time.Duration) { func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, albumID string, ttl time.Duration) {
if time.Since(stat.ModTime()) > 24*time.Hour { if time.Since(stat.ModTime()) > CachedImageValidTime {
i.fetchAndCacheCoverFromServer(albumID, ttl) i.fetchAndCacheCoverFromServer(albumID, ttl)
} }
} }
@@ -129,3 +164,29 @@ func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, albumID string,
func (i *ImageManager) filePathForCover(albumID string) string { func (i *ImageManager) filePathForCover(albumID string) string {
return filepath.Join(i.ensureCoverCacheDir(), fmt.Sprintf("%s.jpg", albumID)) return filepath.Join(i.ensureCoverCacheDir(), fmt.Sprintf("%s.jpg", albumID))
} }
func (i *ImageManager) filePathForArtistImage(id string) string {
return filepath.Join(i.ensureArtistCoverCacheDir(), fmt.Sprintf("%s.jpg", id))
}
func (i *ImageManager) writeJpeg(img image.Image, path string) error {
f, err := os.Create(path)
if err == nil {
defer f.Close()
if err := jpeg.Encode(f, img, nil /*options*/); err != nil {
log.Printf("failed to cache image: %s", err.Error())
return err
}
}
return err
}
func (i *ImageManager) loadLocalImage(path string) (image.Image, bool) {
if f, err := os.Open(path); err == nil {
defer f.Close()
if img, _, err := image.Decode(f); err == nil {
return img, true
}
}
return nil, false
}
+12 -10
View File
@@ -1,8 +1,6 @@
package browsing package browsing
import ( import (
"bytes"
"image"
"log" "log"
"strings" "strings"
"supersonic/backend" "supersonic/backend"
@@ -68,6 +66,9 @@ func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.P
}} }}
a.ExtendBaseWidget(a) a.ExtendBaseWidget(a)
a.header = NewArtistPageHeader(a) a.header = NewArtistPageHeader(a)
if img, ok := im.GetCachedArtistImage(artistID); ok {
a.header.artistImage.SetImage(img, true /*tappable*/)
}
viewToggle := widgets.NewToggleText(0, []string{"Discography", "Top Tracks"}) viewToggle := widgets.NewToggleText(0, []string{"Discography", "Top Tracks"})
viewToggle.SetActivatedLabel(a.activeView) viewToggle.SetActivatedLabel(a.activeView)
viewToggle.OnChanged = a.onViewChange viewToggle.OnChanged = a.onViewChange
@@ -276,6 +277,7 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) {
if info == nil { if info == nil {
return return
} }
if info.Biography != "" { if info.Biography != "" {
segs := util.RichTextSegsFromHTMLString(info.Biography) segs := util.RichTextSegsFromHTMLString(info.Biography)
if len(segs) > 0 { if len(segs) > 0 {
@@ -285,6 +287,7 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) {
} }
} }
} }
a.similarArtists.RemoveAll() a.similarArtists.RemoveAll()
for i, art := range info.SimilarArtist { for i, art := range info.SimilarArtist {
if i == 0 { if i == 0 {
@@ -302,18 +305,17 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) {
a.similarArtists.Add(h) a.similarArtists.Add(h)
} }
a.similarArtists.Refresh() a.similarArtists.Refresh()
if info.LargeImageUrl != "" { if info.LargeImageUrl != "" {
if res, err := fyne.LoadResourceFromURLString(info.LargeImageUrl); err == nil { if a.artistImage.HaveImage() {
im, _, err := image.Decode(bytes.NewReader(res.Content())) _ = a.artistPage.im.RefreshCachedArtistImageIfExpired(a.artistID, info.LargeImageUrl)
if err != nil { } else {
return im, err := a.artistPage.im.FetchAndCacheArtistImage(a.artistID, info.LargeImageUrl)
} if err == nil {
a.artistImage.OnTapped = func() {
a.artistPage.contr.ShowPopUpImage(im)
}
a.artistImage.SetImage(im, true /*tappable*/) a.artistImage.SetImage(im, true /*tappable*/)
} }
} }
}
} }
func (a *ArtistPageHeader) toggleFavorited() { func (a *ArtistPageHeader) toggleFavorited() {
+6
View File
@@ -14,6 +14,7 @@ import (
type ImagePlaceholder struct { type ImagePlaceholder struct {
widget.BaseWidget widget.BaseWidget
container *fyne.Container container *fyne.Container
haveImage bool
minSize float32 minSize float32
OnTapped func() OnTapped func()
@@ -42,6 +43,10 @@ type CanvasImage interface {
SetMinSize(fyne.Size) SetMinSize(fyne.Size)
} }
func (i *ImagePlaceholder) HaveImage() bool {
return i.haveImage
}
func (i *ImagePlaceholder) SetImage(img image.Image, tappable bool) { func (i *ImagePlaceholder) SetImage(img image.Image, tappable bool) {
var cIm CanvasImage var cIm CanvasImage
if tappable { if tappable {
@@ -58,6 +63,7 @@ func (i *ImagePlaceholder) SetImage(img image.Image, tappable bool) {
i.container.RemoveAll() i.container.RemoveAll()
i.container.Add(cIm) i.container.Add(cIm)
i.container.Refresh() i.container.Refresh()
i.haveImage = true
} }
func (i *ImagePlaceholder) onTapped() { func (i *ImagePlaceholder) onTapped() {