add caching of artist images
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
# Change Log
|
||||
|
||||
## Unreleased
|
||||
- [#39](https://github.com/dweymouth/supersonic/issues/39) Add caching of artist images
|
||||
|
||||
## [0.0.1-alpha2] - 2023-03-17
|
||||
|
||||
### Added
|
||||
|
||||
+74
-13
@@ -1,6 +1,7 @@
|
||||
package backend
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
@@ -11,9 +12,12 @@ import (
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"github.com/20after4/configdir"
|
||||
)
|
||||
|
||||
const CachedImageValidTime = 24 * time.Hour
|
||||
|
||||
type ImageManager struct {
|
||||
s *ServerManager
|
||||
baseCacheDir string
|
||||
@@ -77,24 +81,60 @@ func (i *ImageManager) GetFullSizeAlbumCover(albumID string) (image.Image, error
|
||||
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 {
|
||||
path := path.Join(i.baseCacheDir, i.s.ServerID.String(), "covers")
|
||||
configdir.MakePath(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) {
|
||||
// on disc cache
|
||||
path := i.filePathForCover(albumID)
|
||||
if i.ensureCoverCacheDir() != "" {
|
||||
if s, err := os.Stat(path); err == nil {
|
||||
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 {
|
||||
i.thumbnailCache.SetWithTTL(albumID, img, ttl)
|
||||
return img, nil
|
||||
}
|
||||
if img, ok := i.loadLocalImage(path); ok {
|
||||
i.thumbnailCache.SetWithTTL(albumID, img, ttl)
|
||||
return img, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,19 +149,14 @@ func (i *ImageManager) fetchAndCacheCoverFromServer(albumID string, ttl time.Dur
|
||||
}
|
||||
if i.ensureCoverCacheDir() != "" {
|
||||
path := i.filePathForCover(albumID)
|
||||
if f, err := os.Create(path); err == nil {
|
||||
defer f.Close()
|
||||
if err := jpeg.Encode(f, img, nil /*options*/); err != nil {
|
||||
log.Printf("failed to cache image: %s", err.Error())
|
||||
}
|
||||
}
|
||||
_ = i.writeJpeg(img, path)
|
||||
}
|
||||
i.thumbnailCache.SetWithTTL(albumID, img, ttl)
|
||||
return img, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -129,3 +164,29 @@ func (i *ImageManager) checkRefreshLocalCover(stat os.FileInfo, albumID string,
|
||||
func (i *ImageManager) filePathForCover(albumID string) string {
|
||||
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
@@ -1,8 +1,6 @@
|
||||
package browsing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"log"
|
||||
"strings"
|
||||
"supersonic/backend"
|
||||
@@ -68,6 +66,9 @@ func newArtistPage(artistID string, cfg *backend.ArtistPageConfig, pm *backend.P
|
||||
}}
|
||||
a.ExtendBaseWidget(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.SetActivatedLabel(a.activeView)
|
||||
viewToggle.OnChanged = a.onViewChange
|
||||
@@ -276,6 +277,7 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) {
|
||||
if info == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if info.Biography != "" {
|
||||
segs := util.RichTextSegsFromHTMLString(info.Biography)
|
||||
if len(segs) > 0 {
|
||||
@@ -285,6 +287,7 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.similarArtists.RemoveAll()
|
||||
for i, art := range info.SimilarArtist {
|
||||
if i == 0 {
|
||||
@@ -302,16 +305,15 @@ func (a *ArtistPageHeader) UpdateInfo(info *subsonic.ArtistInfo2) {
|
||||
a.similarArtists.Add(h)
|
||||
}
|
||||
a.similarArtists.Refresh()
|
||||
|
||||
if info.LargeImageUrl != "" {
|
||||
if res, err := fyne.LoadResourceFromURLString(info.LargeImageUrl); err == nil {
|
||||
im, _, err := image.Decode(bytes.NewReader(res.Content()))
|
||||
if err != nil {
|
||||
return
|
||||
if a.artistImage.HaveImage() {
|
||||
_ = a.artistPage.im.RefreshCachedArtistImageIfExpired(a.artistID, info.LargeImageUrl)
|
||||
} else {
|
||||
im, err := a.artistPage.im.FetchAndCacheArtistImage(a.artistID, info.LargeImageUrl)
|
||||
if err == nil {
|
||||
a.artistImage.SetImage(im, true /*tappable*/)
|
||||
}
|
||||
a.artistImage.OnTapped = func() {
|
||||
a.artistPage.contr.ShowPopUpImage(im)
|
||||
}
|
||||
a.artistImage.SetImage(im, true /*tappable*/)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
type ImagePlaceholder struct {
|
||||
widget.BaseWidget
|
||||
container *fyne.Container
|
||||
haveImage bool
|
||||
minSize float32
|
||||
|
||||
OnTapped func()
|
||||
@@ -42,6 +43,10 @@ type CanvasImage interface {
|
||||
SetMinSize(fyne.Size)
|
||||
}
|
||||
|
||||
func (i *ImagePlaceholder) HaveImage() bool {
|
||||
return i.haveImage
|
||||
}
|
||||
|
||||
func (i *ImagePlaceholder) SetImage(img image.Image, tappable bool) {
|
||||
var cIm CanvasImage
|
||||
if tappable {
|
||||
@@ -58,6 +63,7 @@ func (i *ImagePlaceholder) SetImage(img image.Image, tappable bool) {
|
||||
i.container.RemoveAll()
|
||||
i.container.Add(cIm)
|
||||
i.container.Refresh()
|
||||
i.haveImage = true
|
||||
}
|
||||
|
||||
func (i *ImagePlaceholder) onTapped() {
|
||||
|
||||
Reference in New Issue
Block a user