mirror of
https://github.com/gabehf/Koito.git
synced 2026-04-22 12:01:52 -07:00
Three bugs causing all artist images to be the same Last.fm placeholder: 1. Last.fm stopped serving real artist images years ago and returns a generic placeholder (hash 2a96cbd8b46e442fc41c2b86b821562f) for every artist. Added filter in selectBestImage to reject URLs containing this known placeholder hash. 2. Provider order had Last.fm before Deezer for artist images. Since Last.fm "succeeded" with the placeholder, Deezer was never reached. Swapped order: Deezer now checked before Last.fm. 3. FetchMissingArtistImages had inverted if/else — aliases were used on error, bare name on success. Fixed condition to err == nil.
189 lines
5.3 KiB
Go
189 lines
5.3 KiB
Go
// package imagesrc defines interfaces for album and artist image providers
|
|
package images
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/gabehf/koito/internal/logger"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type ImageSource struct {
|
|
deezerEnabled bool
|
|
deezerC *DeezerClient
|
|
subsonicEnabled bool
|
|
subsonicC *SubsonicClient
|
|
lastfmEnabled bool
|
|
lastfmC *LastFMClient
|
|
caaEnabled bool
|
|
}
|
|
type ImageSourceOpts struct {
|
|
UserAgent string
|
|
EnableCAA bool
|
|
EnableDeezer bool
|
|
EnableSubsonic bool
|
|
EnableLastFM bool
|
|
}
|
|
|
|
var once sync.Once
|
|
var imgsrc ImageSource
|
|
|
|
type ArtistImageOpts struct {
|
|
Aliases []string
|
|
MBID *uuid.UUID
|
|
}
|
|
|
|
type AlbumImageOpts struct {
|
|
Artists []string
|
|
Album string
|
|
ReleaseMbzID *uuid.UUID
|
|
ReleaseGroupMbzID *uuid.UUID
|
|
}
|
|
|
|
const caaBaseUrl = "https://coverartarchive.org"
|
|
|
|
// all functions are no-op if no providers are enabled
|
|
func Initialize(opts ImageSourceOpts) {
|
|
once.Do(func() {
|
|
if opts.EnableCAA {
|
|
imgsrc.caaEnabled = true
|
|
}
|
|
if opts.EnableDeezer {
|
|
imgsrc.deezerEnabled = true
|
|
imgsrc.deezerC = NewDeezerClient()
|
|
}
|
|
if opts.EnableSubsonic {
|
|
imgsrc.subsonicEnabled = true
|
|
imgsrc.subsonicC = NewSubsonicClient()
|
|
}
|
|
if opts.EnableLastFM {
|
|
imgsrc.lastfmEnabled = true
|
|
imgsrc.lastfmC = NewLastFMClient()
|
|
}
|
|
})
|
|
}
|
|
|
|
func Shutdown() {
|
|
imgsrc.deezerC.Shutdown()
|
|
}
|
|
|
|
func GetArtistImage(ctx context.Context, opts ArtistImageOpts) (string, error) {
|
|
l := logger.FromContext(ctx)
|
|
if imgsrc.subsonicEnabled {
|
|
img, err := imgsrc.subsonicC.GetArtistImage(ctx, opts.MBID, opts.Aliases[0])
|
|
if err != nil {
|
|
l.Debug().Err(err).Msg("GetArtistImage: Could not find artist image from Subsonic")
|
|
} else if img != "" {
|
|
return img, nil
|
|
}
|
|
} else {
|
|
l.Debug().Msg("GetArtistImage: Subsonic image fetching is disabled")
|
|
}
|
|
if imgsrc.deezerEnabled {
|
|
img, err := imgsrc.deezerC.GetArtistImages(ctx, opts.Aliases)
|
|
if err != nil {
|
|
l.Debug().Err(err).Msg("GetArtistImage: Could not find artist image from Deezer")
|
|
} else if img != "" {
|
|
return img, nil
|
|
}
|
|
} else {
|
|
l.Debug().Msg("GetArtistImage: Deezer image fetching is disabled")
|
|
}
|
|
if imgsrc.lastfmEnabled {
|
|
img, err := imgsrc.lastfmC.GetArtistImage(ctx, opts.MBID, opts.Aliases[0])
|
|
if err != nil {
|
|
l.Debug().Err(err).Msg("GetArtistImage: Could not find artist image from LastFM")
|
|
} else if img != "" {
|
|
return img, nil
|
|
}
|
|
} else {
|
|
l.Debug().Msg("GetArtistImage: LastFM image fetching is disabled")
|
|
}
|
|
l.Warn().Msg("GetArtistImage: No image providers are enabled")
|
|
return "", nil
|
|
}
|
|
|
|
func GetAlbumImage(ctx context.Context, opts AlbumImageOpts) (string, error) {
|
|
l := logger.FromContext(ctx)
|
|
if imgsrc.subsonicEnabled {
|
|
img, err := imgsrc.subsonicC.GetAlbumImage(ctx, opts.ReleaseMbzID, opts.Artists[0], opts.Album)
|
|
if err != nil {
|
|
l.Debug().Err(err).Msg("GetAlbumImage: Could not find artist image from Subsonic")
|
|
}
|
|
if img != "" {
|
|
return img, nil
|
|
}
|
|
l.Debug().Msg("Could not find album cover from Subsonic")
|
|
}
|
|
if imgsrc.caaEnabled {
|
|
l.Debug().Msg("Attempting to find album image from CoverArtArchive")
|
|
if opts.ReleaseMbzID != nil && *opts.ReleaseMbzID != uuid.Nil {
|
|
url := fmt.Sprintf(caaBaseUrl+"/release/%s/front", opts.ReleaseMbzID.String())
|
|
resp, err := http.DefaultClient.Head(url)
|
|
if err != nil {
|
|
l.Debug().Err(err).Msg("GetAlbumImage: Could not find artist image from CoverArtArchive with Release MBID")
|
|
} else {
|
|
if resp.StatusCode == 200 {
|
|
return url, nil
|
|
} else {
|
|
l.Debug().Int("status", resp.StatusCode).Msg("GetAlbumImage: Got non-OK response from CoverArtArchive")
|
|
}
|
|
}
|
|
}
|
|
if opts.ReleaseGroupMbzID != nil && *opts.ReleaseGroupMbzID != uuid.Nil {
|
|
url := fmt.Sprintf(caaBaseUrl+"/release-group/%s/front", opts.ReleaseGroupMbzID.String())
|
|
resp, err := http.DefaultClient.Head(url)
|
|
if err != nil {
|
|
l.Debug().Err(err).Msg("GetAlbumImage: Could not find artist image from CoverArtArchive with Release Group MBID")
|
|
}
|
|
if resp.StatusCode == 200 {
|
|
return url, nil
|
|
}
|
|
}
|
|
}
|
|
if imgsrc.lastfmEnabled {
|
|
img, err := imgsrc.lastfmC.GetAlbumImage(ctx, opts.ReleaseMbzID, opts.Artists[0], opts.Album)
|
|
if err != nil {
|
|
l.Debug().Err(err).Msg("GetAlbumImage: Could not find artist image from Subsonic")
|
|
}
|
|
if img != "" {
|
|
return img, nil
|
|
}
|
|
l.Debug().Msg("Could not find album cover from Subsonic")
|
|
}
|
|
if imgsrc.deezerEnabled {
|
|
l.Debug().Msg("Attempting to find album image from Deezer")
|
|
img, err := imgsrc.deezerC.GetAlbumImages(ctx, opts.Artists, opts.Album)
|
|
if err != nil {
|
|
l.Debug().Err(err).Msg("GetAlbumImage: Could not find artist image from Deezer")
|
|
return "", err
|
|
}
|
|
return img, nil
|
|
}
|
|
l.Warn().Msg("GetAlbumImage: No image providers are enabled")
|
|
return "", nil
|
|
}
|
|
|
|
// ValidateImageURL checks if the URL points to a valid image by performing a HEAD request.
|
|
func ValidateImageURL(url string) error {
|
|
resp, err := http.Head(url)
|
|
if err != nil {
|
|
return fmt.Errorf("ValidateImageURL: http.Head: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("ValidateImageURL: HEAD request failed, status code: %d", resp.StatusCode)
|
|
}
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if !strings.HasPrefix(contentType, "image/") {
|
|
return fmt.Errorf("ValidateImageURL: URL does not point to an image, content type: %s", contentType)
|
|
}
|
|
|
|
return nil
|
|
}
|