fix(metadata): verify maximum cover candidates

This commit is contained in:
zarzet
2026-07-29 02:42:46 +07:00
parent 8dcf7fa998
commit 452d9a76cb
3 changed files with 230 additions and 30 deletions
+138 -29
View File
@@ -1,7 +1,11 @@
package gobackend
import (
"bytes"
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"io"
"net/http"
"regexp"
@@ -16,8 +20,11 @@ const (
spotifySizeMax = "ab67616d000082c1"
)
// Deezer CDN supports these sizes: 56, 250, 500, 1000, 1400, 1800
var deezerSizeRegex = regexp.MustCompile(`/(\d+)x(\d+)-\d+-\d+-\d+-\d+\.jpg$`)
// Square CDN covers using this path shape may return an image whose decoded
// dimensions differ from the dimensions advertised in the URL. Max-quality
// selection therefore probes both useful high-resolution variants and checks
// the image headers instead of trusting the filename.
var squareCoverSizeRegex = regexp.MustCompile(`/(\d+)x(\d+)-\d+-\d+-\d+-\d+\.jpg$`)
var tidalSizeRegex = regexp.MustCompile(`/\d+x\d+\.jpg$`)
@@ -42,27 +49,137 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) {
GoLog("[Cover] Upgraded 300x300 → 640x640")
}
if maxQuality {
maxURL := upgradeToMaxQuality(downloadURL)
if maxURL != downloadURL {
downloadURL = maxURL
if strings.Contains(coverURL, "scdn.co") || strings.Contains(coverURL, "spotifycdn") {
GoLog("[Cover] Spotify: upgraded to max resolution (~2000x2000)")
}
if !maxQuality {
GoLog("[Cover] Final URL: %s", downloadURL)
data, err := fetchCoverCached(downloadURL)
if err != nil {
return nil, err
}
return append([]byte(nil), data...), nil
}
GoLog("[Cover] Final URL: %s", downloadURL)
data, err := fetchCoverCached(downloadURL)
candidates := maxQualityCoverCandidateURLs(downloadURL)
data, selectedURL, width, height, err := fetchBestCoverCandidate(candidates)
if err != nil {
return nil, err
// A CDN can reject an upgraded size while the provider-supplied URL is
// still valid. Preserve that URL as the final fallback.
if len(candidates) == 1 && candidates[0] == downloadURL {
return nil, err
}
data, err = fetchCoverCached(downloadURL)
if err != nil {
return nil, err
}
selectedURL = downloadURL
width, height = coverDimensions(data)
}
GoLog("[Cover] Selected URL: %s (%dx%d, %d KB)", selectedURL, width, height, len(data)/1024)
// Cached bytes are shared across goroutines and must never be mutated;
// hand callers their own copy.
return append([]byte(nil), data...), nil
}
type fetchedCoverCandidate struct {
url string
data []byte
width, height int
err error
}
func maxQualityCoverCandidateURLs(coverURL string) []string {
upgraded := upgradeToMaxQuality(coverURL)
candidates := []string{upgraded}
// This is deliberately based on the URL capability rather than a provider
// or extension ID. Any metadata source returning the same square-cover URL
// shape receives the same verified candidate selection.
if squareCoverSizeRegex.MatchString(coverURL) {
candidate1500 := squareCoverSizeRegex.ReplaceAllString(
coverURL,
"/1500x1500-000000-80-0-0.jpg",
)
if candidate1500 != upgraded {
candidates = append(candidates, candidate1500)
}
}
return uniqueNonEmptyStrings(candidates)
}
func uniqueNonEmptyStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}
func fetchBestCoverCandidate(urls []string) ([]byte, string, int, int, error) {
if len(urls) == 0 {
return nil, "", 0, 0, fmt.Errorf("no cover candidates available")
}
results := make(chan fetchedCoverCandidate, len(urls))
for _, candidateURL := range urls {
go func(url string) {
data, err := fetchCoverCached(url)
width, height := coverDimensions(data)
results <- fetchedCoverCandidate{
url: url, data: data, width: width, height: height, err: err,
}
}(candidateURL)
}
var best *fetchedCoverCandidate
var firstErr error
for range urls {
candidate := <-results
if candidate.err != nil || len(candidate.data) == 0 {
if firstErr == nil {
firstErr = candidate.err
}
continue
}
if best == nil || coverCandidateBetter(candidate, *best) {
copy := candidate
best = &copy
}
}
if best == nil {
if firstErr == nil {
firstErr = fmt.Errorf("cover candidates returned no image data")
}
return nil, "", 0, 0, firstErr
}
return best.data, best.url, best.width, best.height, nil
}
func coverDimensions(data []byte) (int, int) {
if len(data) == 0 {
return 0, 0
}
config, _, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil || config.Width <= 0 || config.Height <= 0 {
return 0, 0
}
return config.Width, config.Height
}
func coverCandidateBetter(candidate, current fetchedCoverCandidate) bool {
candidatePixels := int64(candidate.width) * int64(candidate.height)
currentPixels := int64(current.width) * int64(current.height)
if candidatePixels != currentPixels {
return candidatePixels > currentPixels
}
return len(candidate.data) > len(current.data)
}
const (
coverCacheMaxBytes = 24 * 1024 * 1024
coverCacheTTL = 15 * time.Minute
@@ -185,16 +302,8 @@ func fetchCoverBytes(downloadURL string) ([]byte, error) {
return nil, fmt.Errorf("failed to read cover data: %w", err)
}
sizeKB := len(data) / 1024
var resolution string
if sizeKB > 200 {
resolution = "~2000x2000 (hi-res)"
} else if sizeKB > 50 {
resolution = "~640x640"
} else {
resolution = "~300x300"
}
GoLog("[Cover] Downloaded %d KB (%s)", sizeKB, resolution)
width, height := coverDimensions(data)
GoLog("[Cover] Downloaded %d KB (%dx%d)", len(data)/1024, width, height)
return data, nil
}
@@ -204,8 +313,8 @@ func upgradeToMaxQuality(coverURL string) string {
return strings.Replace(coverURL, spotifySize640, spotifySizeMax, 1)
}
if strings.Contains(coverURL, "cdn-images.dzcdn.net") {
return upgradeDeezerCover(coverURL)
if squareCoverSizeRegex.MatchString(coverURL) {
return upgradeSquareCover(coverURL)
}
if strings.Contains(coverURL, "resources.tidal.com") {
@@ -219,14 +328,14 @@ func upgradeToMaxQuality(coverURL string) string {
return coverURL
}
func upgradeDeezerCover(coverURL string) string {
if !strings.Contains(coverURL, "cdn-images.dzcdn.net") {
func upgradeSquareCover(coverURL string) string {
if !squareCoverSizeRegex.MatchString(coverURL) {
return coverURL
}
upgraded := deezerSizeRegex.ReplaceAllString(coverURL, "/1800x1800-000000-80-0-0.jpg")
upgraded := squareCoverSizeRegex.ReplaceAllString(coverURL, "/1900x1900-000000-80-0-0.jpg")
if upgraded != coverURL {
GoLog("[Cover] Deezer: upgraded to 1800x1800")
GoLog("[Cover] Square CDN: probing 1900x1900 and 1500x1500")
}
return upgraded
}
+91
View File
@@ -1,12 +1,33 @@
package gobackend
import (
"bytes"
"errors"
"image"
"image/color"
"image/png"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
)
func testPNG(t *testing.T, width, height int) []byte {
t.Helper()
img := image.NewRGBA(image.Rect(0, 0, width, height))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
img.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: 100, A: 255})
}
}
var buffer bytes.Buffer
if err := png.Encode(&buffer, img); err != nil {
t.Fatalf("encode test cover: %v", err)
}
return buffer.Bytes()
}
func resetCoverCache() {
coverMu.Lock()
coverCache = map[string]*coverCacheEntry{}
@@ -94,3 +115,73 @@ func TestFetchCoverCachedTTLExpiry(t *testing.T) {
t.Fatalf("expected refetch after TTL expiry, got %d fetches", got)
}
}
func TestMaxQualityCoverCandidatesProbe1900And1500(t *testing.T) {
url := "https://cdn-images.dzcdn.net/images/cover/abc/1000x1000-000000-80-0-0.jpg"
candidates := maxQualityCoverCandidateURLs(url)
if len(candidates) != 2 {
t.Fatalf("expected two candidates, got %#v", candidates)
}
if !strings.Contains(candidates[0], "1900x1900") {
t.Fatalf("first candidate = %q", candidates[0])
}
if !strings.Contains(candidates[1], "1500x1500") {
t.Fatalf("second candidate = %q", candidates[1])
}
}
func TestDownloadCoverSelectsDecodedDimensionsBeforeByteSize(t *testing.T) {
orig := coverFetch
defer func() { coverFetch = orig }()
resetCoverCache()
advertised1900ButSmaller := append(testPNG(t, 12, 12), bytes.Repeat([]byte{0}, 4096)...)
actualLargerDimensions := testPNG(t, 15, 15)
coverFetch = func(url string) ([]byte, error) {
switch {
case strings.Contains(url, "1900x1900"):
return advertised1900ButSmaller, nil
case strings.Contains(url, "1500x1500"):
return actualLargerDimensions, nil
default:
return nil, errors.New("unexpected URL")
}
}
url := "https://cdn-images.dzcdn.net/images/cover/abc/1000x1000-000000-80-0-0.jpg"
got, err := downloadCoverToMemory(url, true)
if err != nil {
t.Fatalf("download max cover: %v", err)
}
if !bytes.Equal(got, actualLargerDimensions) {
t.Fatal("expected decoded 15x15 candidate instead of larger-byte 12x12 candidate")
}
}
func TestBestCoverCandidateUsesByteSizeAsDimensionTiebreaker(t *testing.T) {
orig := coverFetch
defer func() { coverFetch = orig }()
resetCoverCache()
base := testPNG(t, 10, 10)
largerFile := append(append([]byte(nil), base...), bytes.Repeat([]byte{0}, 512)...)
coverFetch = func(url string) ([]byte, error) {
if strings.Contains(url, "large") {
return largerFile, nil
}
return base, nil
}
got, selected, width, height, err := fetchBestCoverCandidate(
[]string{"https://covers.test/small", "https://covers.test/large"},
)
if err != nil {
t.Fatalf("select cover: %v", err)
}
if selected != "https://covers.test/large" || width != 10 || height != 10 {
t.Fatalf("selected=%q dimensions=%dx%d", selected, width, height)
}
if !bytes.Equal(got, largerFile) {
t.Fatal("expected larger file when decoded dimensions are equal")
}
}
@@ -72,7 +72,7 @@ func TestExtensionHealthClassificationAndValidation(t *testing.T) {
}
func TestCoverAndIDHSHelpers(t *testing.T) {
if got := upgradeToMaxQuality("https://cdn-images.dzcdn.net/images/cover/abc/500x500-000000-80-0-0.jpg"); !strings.Contains(got, "1800x1800") {
if got := upgradeToMaxQuality("https://cdn-images.dzcdn.net/images/cover/abc/500x500-000000-80-0-0.jpg"); !strings.Contains(got, "1900x1900") {
t.Fatalf("deezer cover = %q", got)
}
if got := upgradeToMaxQuality("https://resources.tidal.com/images/id/320x320.jpg"); !strings.Contains(got, "origin.jpg") {