mirror of
https://github.com/navidrome/navidrome.git
synced 2026-04-28 03:19:38 +00:00
fix(artwork): refresh stale artist image URLs on expiry (#5267)
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-1 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-2 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-3 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-4 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-5 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-6 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-7 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-8 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-9 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-10 (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-1 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-2 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-3 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-4 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-5 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-6 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-7 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-8 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-9 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-10 (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
* fix(external): refresh stale artist image URLs on expiry ArtistImage() was serving cached image URLs from the database indefinitely, ignoring ExternalInfoUpdatedAt. When users changed agent configuration (e.g. disabling Deezer), old URLs persisted because only the UpdateArtistInfo code path checked the TTL. Now ArtistImage() checks the expiry and enqueues a background refresh when the cached info is stale, matching the pattern used by refreshArtistInfo(). The stale URL is still returned immediately to avoid blocking clients. Fixes #5266 * test: add expired artist image info test with log assertion Verify that ArtistImage() enqueues a background refresh when cached info is expired, by capturing log output and checking for the expected debug message. Also asserts the stale URL is returned immediately without calling the agent. Signed-off-by: Deluan <deluan@navidrome.org> * fix: only enqueue refresh when returning a stale cached URL Move the expiry check to the else branch so we only enqueue a background refresh when a cached image URL exists and is being returned. This avoids doubling external API calls when the URL is empty (synchronous fetch) but ExternalInfoUpdatedAt is old. --------- Signed-off-by: Deluan <deluan@navidrome.org>
This commit is contained in:
parent
420d2c8e5a
commit
0f6a076dca
2 changed files with 73 additions and 2 deletions
10
core/external/provider.go
vendored
10
core/external/provider.go
vendored
|
|
@ -374,8 +374,6 @@ func (e *provider) ArtistImage(ctx context.Context, id string) (*url.URL, error)
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Use already-stored image URL if available, avoiding expensive external API calls.
|
||||
// If the info is expired, the background refresh (via UpdateArtistInfo/artistQueue) will update it.
|
||||
imageUrl := artist.ArtistImageUrl()
|
||||
if imageUrl == "" {
|
||||
// No cached URL — must fetch from external source synchronously
|
||||
|
|
@ -385,6 +383,14 @@ func (e *provider) ArtistImage(ctx context.Context, id string) (*url.URL, error)
|
|||
return nil, ctx.Err()
|
||||
}
|
||||
imageUrl = artist.ArtistImageUrl()
|
||||
} else {
|
||||
// If cached info is expired, enqueue a background refresh so that config changes
|
||||
// (e.g. disabling an agent) take effect without waiting for a full artist info refresh.
|
||||
updatedAt := V(artist.ExternalInfoUpdatedAt)
|
||||
if !updatedAt.IsZero() && time.Since(updatedAt) > conf.Server.DevArtistInfoTimeToLive {
|
||||
log.Debug(ctx, "Artist image info expired, enqueuing background refresh", "artist", artist.Name(), "updatedAt", updatedAt)
|
||||
e.artistQueue.enqueue(&artist)
|
||||
}
|
||||
}
|
||||
|
||||
if imageUrl == "" {
|
||||
|
|
|
|||
65
core/external/provider_artistimage_test.go
vendored
65
core/external/provider_artistimage_test.go
vendored
|
|
@ -1,14 +1,17 @@
|
|||
package external_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/conf"
|
||||
"github.com/navidrome/navidrome/conf/configtest"
|
||||
"github.com/navidrome/navidrome/core/agents"
|
||||
. "github.com/navidrome/navidrome/core/external"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
"github.com/navidrome/navidrome/model"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
|
|
@ -266,6 +269,68 @@ var _ = Describe("Provider - ArtistImage", func() {
|
|||
mockImageAgent.AssertCalled(GinkgoT(), "GetArtistImages", ctx, "artist-1", "Artist One", "")
|
||||
})
|
||||
|
||||
It("returns cached URL and does not call agent when info is not expired", func() {
|
||||
// Arrange: artist has a cached image URL with recent ExternalInfoUpdatedAt
|
||||
recentTime := time.Now().Add(-1 * time.Minute)
|
||||
cachedArtist := &model.Artist{
|
||||
ID: "artist-cached",
|
||||
Name: "Cached Artist",
|
||||
LargeImageUrl: "http://example.com/cached-large.jpg",
|
||||
ExternalInfoUpdatedAt: &recentTime,
|
||||
}
|
||||
mockArtistRepo.On("Get", "artist-cached").Return(cachedArtist, nil).Maybe()
|
||||
expectedURL, _ := url.Parse("http://example.com/cached-large.jpg")
|
||||
|
||||
// Capture log output
|
||||
var logBuf bytes.Buffer
|
||||
log.SetOutput(&logBuf)
|
||||
defer log.SetOutput(GinkgoWriter)
|
||||
log.SetLevel(log.LevelDebug)
|
||||
|
||||
// Act
|
||||
imgURL, err := provider.ArtistImage(ctx, "artist-cached")
|
||||
|
||||
// Assert
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(imgURL).To(Equal(expectedURL))
|
||||
mockImageAgent.AssertNotCalled(GinkgoT(), "GetArtistImages", mock.Anything, "artist-cached", mock.Anything, mock.Anything)
|
||||
|
||||
// Assert: background refresh was NOT enqueued
|
||||
Expect(logBuf.String()).ToNot(ContainSubstring("Artist image info expired, enqueuing background refresh"))
|
||||
|
||||
})
|
||||
|
||||
It("returns stale URL and enqueues refresh when info is expired", func() {
|
||||
// Arrange
|
||||
conf.Server.DevArtistInfoTimeToLive = 1 * time.Nanosecond
|
||||
expiredTime := time.Now().Add(-1 * time.Hour)
|
||||
staleArtist := &model.Artist{
|
||||
ID: "artist-expired",
|
||||
Name: "Expired Artist",
|
||||
LargeImageUrl: "http://example.com/expired-large.jpg",
|
||||
ExternalInfoUpdatedAt: &expiredTime,
|
||||
}
|
||||
mockArtistRepo.On("Get", "artist-expired").Return(staleArtist, nil).Maybe()
|
||||
expectedURL, _ := url.Parse("http://example.com/expired-large.jpg")
|
||||
|
||||
// Capture log output
|
||||
var logBuf bytes.Buffer
|
||||
log.SetOutput(&logBuf)
|
||||
defer log.SetOutput(GinkgoWriter)
|
||||
log.SetLevel(log.LevelDebug)
|
||||
|
||||
// Act
|
||||
imgURL, err := provider.ArtistImage(ctx, "artist-expired")
|
||||
|
||||
// Assert: returns stale URL immediately, no agent call
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(imgURL).To(Equal(expectedURL))
|
||||
mockImageAgent.AssertNotCalled(GinkgoT(), "GetArtistImages", mock.Anything, "artist-expired", mock.Anything, mock.Anything)
|
||||
|
||||
// Assert: background refresh was enqueued
|
||||
Expect(logBuf.String()).To(ContainSubstring("Artist image info expired, enqueuing background refresh"))
|
||||
})
|
||||
|
||||
Context("Unicode handling in artist names", func() {
|
||||
var artistWithEnDash *model.Artist
|
||||
var expectedURL *url.URL
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue