diff --git a/coverart.go b/coverart.go index a94a888..8ac5446 100644 --- a/coverart.go +++ b/coverart.go @@ -36,6 +36,57 @@ func headCoverArt(url string) string { return location } +const ( + caaCacheTTLHit int64 = 24 * 60 * 60 // 24 hours for resolved artwork + caaCacheTTLMiss int64 = 4 * 60 * 60 // 4 hours for misses +) + +// getImageViaCoverArt checks the Cover Art Archive for album artwork. +// Tries the release first, then falls back to the release group. +// Returns the archive.org image URL on success, "" on failure. +func getImageViaCoverArt(mbzAlbumID, mbzReleaseGroupID string) string { + if mbzAlbumID == "" && mbzReleaseGroupID == "" { + return "" + } + + // Determine cache key: use album ID when available, otherwise release group ID + cacheKey := "caa.artwork." + mbzAlbumID + if mbzAlbumID == "" { + cacheKey = "caa.artwork.rg." + mbzReleaseGroupID + } + + // Check cache + cachedURL, exists, err := host.CacheGetString(cacheKey) + if err == nil && exists { + pdk.Log(pdk.LogDebug, fmt.Sprintf("CAA cache hit for %s", cacheKey)) + return cachedURL + } + + // Try release first + var imageURL string + if mbzAlbumID != "" { + imageURL = headCoverArt(fmt.Sprintf("https://coverartarchive.org/release/%s/front-500", mbzAlbumID)) + } + + // Fall back to release group + if imageURL == "" && mbzReleaseGroupID != "" { + imageURL = headCoverArt(fmt.Sprintf("https://coverartarchive.org/release-group/%s/front-500", mbzReleaseGroupID)) + } + + // Cache the result (hit or miss) + ttl := caaCacheTTLHit + if imageURL == "" { + ttl = caaCacheTTLMiss + } + _ = host.CacheSetString(cacheKey, imageURL, ttl) + + if imageURL != "" { + pdk.Log(pdk.LogDebug, fmt.Sprintf("CAA resolved artwork for %s: %s", cacheKey, imageURL)) + } + + return imageURL +} + // uguu.se API response type uguuResponse struct { Success bool `json:"success"` diff --git a/coverart_test.go b/coverart_test.go index c808984..3f8a7c9 100644 --- a/coverart_test.go +++ b/coverart_test.go @@ -161,3 +161,98 @@ var _ = Describe("getImageURL", func() { }) }) }) + +var _ = Describe("getImageViaCoverArt", func() { + BeforeEach(func() { + pdk.ResetMock() + host.CacheMock.ExpectedCalls = nil + host.CacheMock.Calls = nil + host.HTTPMock.ExpectedCalls = nil + host.HTTPMock.Calls = nil + pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe() + }) + + It("returns cached URL on cache hit", func() { + host.CacheMock.On("GetString", "caa.artwork.album-123").Return("https://archive.org/cached.jpg", true, nil) + + result := getImageViaCoverArt("album-123", "rg-456") + Expect(result).To(Equal("https://archive.org/cached.jpg")) + host.HTTPMock.AssertNotCalled(GinkgoT(), "Send", mock.Anything) + }) + + It("returns empty on cache hit with empty string (known miss)", func() { + host.CacheMock.On("GetString", "caa.artwork.album-123").Return("", true, nil) + + result := getImageViaCoverArt("album-123", "rg-456") + Expect(result).To(BeEmpty()) + host.HTTPMock.AssertNotCalled(GinkgoT(), "Send", mock.Anything) + }) + + It("returns release URL on 307 and caches it", func() { + host.CacheMock.On("GetString", "caa.artwork.album-123").Return("", false, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release/album-123/front-500" + })).Return(&host.HTTPResponse{ + StatusCode: 307, + Headers: map[string]string{"Location": "https://archive.org/release-art.jpg"}, + }, nil) + host.CacheMock.On("SetString", "caa.artwork.album-123", "https://archive.org/release-art.jpg", int64(86400)).Return(nil) + + result := getImageViaCoverArt("album-123", "rg-456") + Expect(result).To(Equal("https://archive.org/release-art.jpg")) + host.CacheMock.AssertCalled(GinkgoT(), "SetString", "caa.artwork.album-123", "https://archive.org/release-art.jpg", int64(86400)) + }) + + It("falls back to release-group when release returns 404", func() { + host.CacheMock.On("GetString", "caa.artwork.album-123").Return("", false, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release/album-123/front-500" + })).Return(&host.HTTPResponse{StatusCode: 404}, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release-group/rg-456/front-500" + })).Return(&host.HTTPResponse{ + StatusCode: 307, + Headers: map[string]string{"Location": "https://archive.org/rg-art.jpg"}, + }, nil) + host.CacheMock.On("SetString", "caa.artwork.album-123", "https://archive.org/rg-art.jpg", int64(86400)).Return(nil) + + result := getImageViaCoverArt("album-123", "rg-456") + Expect(result).To(Equal("https://archive.org/rg-art.jpg")) + }) + + It("caches empty string when both release and release-group fail", func() { + host.CacheMock.On("GetString", "caa.artwork.album-123").Return("", false, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release/album-123/front-500" + })).Return(&host.HTTPResponse{StatusCode: 404}, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release-group/rg-456/front-500" + })).Return(&host.HTTPResponse{StatusCode: 404}, nil) + host.CacheMock.On("SetString", "caa.artwork.album-123", "", int64(14400)).Return(nil) + + result := getImageViaCoverArt("album-123", "rg-456") + Expect(result).To(BeEmpty()) + host.CacheMock.AssertCalled(GinkgoT(), "SetString", "caa.artwork.album-123", "", int64(14400)) + }) + + It("tries only release-group when MBZAlbumID is empty", func() { + host.CacheMock.On("GetString", "caa.artwork.rg.rg-456").Return("", false, nil) + host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool { + return req.URL == "https://coverartarchive.org/release-group/rg-456/front-500" + })).Return(&host.HTTPResponse{ + StatusCode: 307, + Headers: map[string]string{"Location": "https://archive.org/rg-art.jpg"}, + }, nil) + host.CacheMock.On("SetString", "caa.artwork.rg.rg-456", "https://archive.org/rg-art.jpg", int64(86400)).Return(nil) + + result := getImageViaCoverArt("", "rg-456") + Expect(result).To(Equal("https://archive.org/rg-art.jpg")) + }) + + It("returns empty when both IDs are empty", func() { + result := getImageViaCoverArt("", "") + Expect(result).To(BeEmpty()) + host.HTTPMock.AssertNotCalled(GinkgoT(), "Send", mock.Anything) + host.CacheMock.AssertNotCalled(GinkgoT(), "GetString", mock.Anything) + }) +})