mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-07 21:48:18 -08:00
parent
56ac73d12b
commit
e7ba34710c
5 changed files with 345 additions and 3 deletions
|
|
@ -82,6 +82,9 @@ If the environment variable is defined without **and** with the suffix at the sa
|
||||||
If Koito is unable to validate your Subsonic configuration, it will fail to start. If you notice your container isn't running after
|
If Koito is unable to validate your Subsonic configuration, it will fail to start. If you notice your container isn't running after
|
||||||
changing these parameters, check the logs!
|
changing these parameters, check the logs!
|
||||||
:::
|
:::
|
||||||
|
##### KOITO_LASTFM_API_KEY
|
||||||
|
- Required: `false`
|
||||||
|
- Description: Your LastFM API key, which will be used for fetching images if provided. You can get an API key [here](https://www.last.fm/api/authentication),
|
||||||
##### KOITO_SKIP_IMPORT
|
##### KOITO_SKIP_IMPORT
|
||||||
- Default: `false`
|
- Default: `false`
|
||||||
- Description: Skips running the importer on startup.
|
- Description: Skips running the importer on startup.
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,7 @@ func Run(
|
||||||
EnableCAA: !cfg.CoverArtArchiveDisabled(),
|
EnableCAA: !cfg.CoverArtArchiveDisabled(),
|
||||||
EnableDeezer: !cfg.DeezerDisabled(),
|
EnableDeezer: !cfg.DeezerDisabled(),
|
||||||
EnableSubsonic: cfg.SubsonicEnabled(),
|
EnableSubsonic: cfg.SubsonicEnabled(),
|
||||||
|
EnableLastFM: cfg.LastFMApiKey() != "",
|
||||||
})
|
})
|
||||||
l.Info().Msg("Engine: Image sources initialized")
|
l.Info().Msg("Engine: Image sources initialized")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ const (
|
||||||
DISABLE_MUSICBRAINZ_ENV = "KOITO_DISABLE_MUSICBRAINZ"
|
DISABLE_MUSICBRAINZ_ENV = "KOITO_DISABLE_MUSICBRAINZ"
|
||||||
SUBSONIC_URL_ENV = "KOITO_SUBSONIC_URL"
|
SUBSONIC_URL_ENV = "KOITO_SUBSONIC_URL"
|
||||||
SUBSONIC_PARAMS_ENV = "KOITO_SUBSONIC_PARAMS"
|
SUBSONIC_PARAMS_ENV = "KOITO_SUBSONIC_PARAMS"
|
||||||
|
LASTFM_API_KEY_ENV = "KOITO_LASTFM_API_KEY"
|
||||||
SKIP_IMPORT_ENV = "KOITO_SKIP_IMPORT"
|
SKIP_IMPORT_ENV = "KOITO_SKIP_IMPORT"
|
||||||
ALLOWED_HOSTS_ENV = "KOITO_ALLOWED_HOSTS"
|
ALLOWED_HOSTS_ENV = "KOITO_ALLOWED_HOSTS"
|
||||||
CORS_ORIGINS_ENV = "KOITO_CORS_ALLOWED_ORIGINS"
|
CORS_ORIGINS_ENV = "KOITO_CORS_ALLOWED_ORIGINS"
|
||||||
|
|
@ -72,6 +73,7 @@ type config struct {
|
||||||
disableMusicBrainz bool
|
disableMusicBrainz bool
|
||||||
subsonicUrl string
|
subsonicUrl string
|
||||||
subsonicParams string
|
subsonicParams string
|
||||||
|
lastfmApiKey string
|
||||||
subsonicEnabled bool
|
subsonicEnabled bool
|
||||||
skipImport bool
|
skipImport bool
|
||||||
fetchImageDuringImport bool
|
fetchImageDuringImport bool
|
||||||
|
|
@ -165,6 +167,7 @@ func loadConfig(getenv func(string) string, version string) (*config, error) {
|
||||||
if cfg.subsonicEnabled && (cfg.subsonicUrl == "" || cfg.subsonicParams == "") {
|
if cfg.subsonicEnabled && (cfg.subsonicUrl == "" || cfg.subsonicParams == "") {
|
||||||
return nil, fmt.Errorf("loadConfig: invalid configuration: both %s and %s must be set in order to use subsonic image fetching", SUBSONIC_URL_ENV, SUBSONIC_PARAMS_ENV)
|
return nil, fmt.Errorf("loadConfig: invalid configuration: both %s and %s must be set in order to use subsonic image fetching", SUBSONIC_URL_ENV, SUBSONIC_PARAMS_ENV)
|
||||||
}
|
}
|
||||||
|
cfg.lastfmApiKey = getenv(LASTFM_API_KEY_ENV)
|
||||||
cfg.skipImport = parseBool(getenv(SKIP_IMPORT_ENV))
|
cfg.skipImport = parseBool(getenv(SKIP_IMPORT_ENV))
|
||||||
|
|
||||||
cfg.userAgent = fmt.Sprintf("Koito %s (contact@koito.io)", version)
|
cfg.userAgent = fmt.Sprintf("Koito %s (contact@koito.io)", version)
|
||||||
|
|
@ -361,6 +364,12 @@ func SubsonicParams() string {
|
||||||
return globalConfig.subsonicParams
|
return globalConfig.subsonicParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LastFMApiKey() string {
|
||||||
|
lock.RLock()
|
||||||
|
defer lock.RUnlock()
|
||||||
|
return globalConfig.lastfmApiKey
|
||||||
|
}
|
||||||
|
|
||||||
func SkipImport() bool {
|
func SkipImport() bool {
|
||||||
lock.RLock()
|
lock.RLock()
|
||||||
defer lock.RUnlock()
|
defer lock.RUnlock()
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ type ImageSource struct {
|
||||||
deezerC *DeezerClient
|
deezerC *DeezerClient
|
||||||
subsonicEnabled bool
|
subsonicEnabled bool
|
||||||
subsonicC *SubsonicClient
|
subsonicC *SubsonicClient
|
||||||
|
lastfmEnabled bool
|
||||||
|
lastfmC *LastFMClient
|
||||||
caaEnabled bool
|
caaEnabled bool
|
||||||
}
|
}
|
||||||
type ImageSourceOpts struct {
|
type ImageSourceOpts struct {
|
||||||
|
|
@ -24,6 +26,7 @@ type ImageSourceOpts struct {
|
||||||
EnableCAA bool
|
EnableCAA bool
|
||||||
EnableDeezer bool
|
EnableDeezer bool
|
||||||
EnableSubsonic bool
|
EnableSubsonic bool
|
||||||
|
EnableLastFM bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var once sync.Once
|
var once sync.Once
|
||||||
|
|
@ -57,6 +60,10 @@ func Initialize(opts ImageSourceOpts) {
|
||||||
imgsrc.subsonicEnabled = true
|
imgsrc.subsonicEnabled = true
|
||||||
imgsrc.subsonicC = NewSubsonicClient()
|
imgsrc.subsonicC = NewSubsonicClient()
|
||||||
}
|
}
|
||||||
|
if opts.EnableLastFM {
|
||||||
|
imgsrc.lastfmEnabled = true
|
||||||
|
imgsrc.lastfmC = NewLastFMClient()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,6 +83,16 @@ func GetArtistImage(ctx context.Context, opts ArtistImageOpts) (string, error) {
|
||||||
} else {
|
} else {
|
||||||
l.Debug().Msg("GetArtistImage: Subsonic image fetching is disabled")
|
l.Debug().Msg("GetArtistImage: Subsonic 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")
|
||||||
|
}
|
||||||
if imgsrc.deezerEnabled {
|
if imgsrc.deezerEnabled {
|
||||||
img, err := imgsrc.deezerC.GetArtistImages(ctx, opts.Aliases)
|
img, err := imgsrc.deezerC.GetArtistImages(ctx, opts.Aliases)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -90,6 +107,7 @@ func GetArtistImage(ctx context.Context, opts ArtistImageOpts) (string, error) {
|
||||||
l.Warn().Msg("GetArtistImage: No image providers are enabled")
|
l.Warn().Msg("GetArtistImage: No image providers are enabled")
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAlbumImage(ctx context.Context, opts AlbumImageOpts) (string, error) {
|
func GetAlbumImage(ctx context.Context, opts AlbumImageOpts) (string, error) {
|
||||||
l := logger.FromContext(ctx)
|
l := logger.FromContext(ctx)
|
||||||
if imgsrc.subsonicEnabled {
|
if imgsrc.subsonicEnabled {
|
||||||
|
|
@ -109,9 +127,12 @@ func GetAlbumImage(ctx context.Context, opts AlbumImageOpts) (string, error) {
|
||||||
resp, err := http.DefaultClient.Head(url)
|
resp, err := http.DefaultClient.Head(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debug().Err(err).Msg("GetAlbumImage: Could not find artist image from CoverArtArchive with Release MBID")
|
l.Debug().Err(err).Msg("GetAlbumImage: Could not find artist image from CoverArtArchive with Release MBID")
|
||||||
}
|
} else {
|
||||||
if resp.StatusCode == 200 {
|
if resp.StatusCode == 200 {
|
||||||
return url, nil
|
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 {
|
if opts.ReleaseGroupMbzID != nil && *opts.ReleaseGroupMbzID != uuid.Nil {
|
||||||
|
|
@ -125,6 +146,16 @@ func GetAlbumImage(ctx context.Context, opts AlbumImageOpts) (string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 {
|
if imgsrc.deezerEnabled {
|
||||||
l.Debug().Msg("Attempting to find album image from Deezer")
|
l.Debug().Msg("Attempting to find album image from Deezer")
|
||||||
img, err := imgsrc.deezerC.GetAlbumImages(ctx, opts.Artists, opts.Album)
|
img, err := imgsrc.deezerC.GetAlbumImages(ctx, opts.Artists, opts.Album)
|
||||||
|
|
|
||||||
298
internal/images/lastfm.go
Normal file
298
internal/images/lastfm.go
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
package images
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gabehf/koito/internal/cfg"
|
||||||
|
"github.com/gabehf/koito/internal/logger"
|
||||||
|
"github.com/gabehf/koito/queue"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// i told gemini to write this cuz i figured it would be simple enough and
|
||||||
|
// it looks like it just works? maybe ai is actually worth one quintillion gallons of water
|
||||||
|
|
||||||
|
type LastFMClient struct {
|
||||||
|
apiKey string
|
||||||
|
baseUrl string
|
||||||
|
userAgent string
|
||||||
|
requestQueue *queue.RequestQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastFM JSON structures use "#text" for the value of XML-mapped fields
|
||||||
|
type lastFMImage struct {
|
||||||
|
URL string `json:"#text"`
|
||||||
|
Size string `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type lastFMAlbumResponse struct {
|
||||||
|
Album struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Image []lastFMImage `json:"image"`
|
||||||
|
} `json:"album"`
|
||||||
|
Error int `json:"error"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type lastFMArtistResponse struct {
|
||||||
|
Artist struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Image []lastFMImage `json:"image"`
|
||||||
|
} `json:"artist"`
|
||||||
|
Error int `json:"error"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
lastFMApiBaseUrl = "http://ws.audioscrobbler.com/2.0/"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewLastFMClient() *LastFMClient {
|
||||||
|
ret := new(LastFMClient)
|
||||||
|
ret.apiKey = cfg.LastFMApiKey()
|
||||||
|
ret.baseUrl = lastFMApiBaseUrl
|
||||||
|
ret.userAgent = cfg.UserAgent()
|
||||||
|
ret.requestQueue = queue.NewRequestQueue(5, 5)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LastFMClient) queue(ctx context.Context, req *http.Request) ([]byte, error) {
|
||||||
|
l := logger.FromContext(ctx)
|
||||||
|
req.Header.Set("User-Agent", c.userAgent)
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resultChan := c.requestQueue.Enqueue(func(client *http.Client, done chan<- queue.RequestResult) {
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
l.Debug().Err(err).Str("url", req.URL.String()).Msg("Failed to contact LastFM")
|
||||||
|
done <- queue.RequestResult{Err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// LastFM might return 200 OK even for API errors (like "Artist not found"),
|
||||||
|
// so we rely on parsing the JSON body for logic errors later,
|
||||||
|
// but we still check for HTTP protocol failures here.
|
||||||
|
if resp.StatusCode >= 500 {
|
||||||
|
err = fmt.Errorf("received server error from LastFM: %s", resp.Status)
|
||||||
|
done <- queue.RequestResult{Body: nil, Err: err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
done <- queue.RequestResult{Body: body, Err: err}
|
||||||
|
})
|
||||||
|
|
||||||
|
result := <-resultChan
|
||||||
|
return result.Body, result.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LastFMClient) getEntity(ctx context.Context, params url.Values, result any) error {
|
||||||
|
l := logger.FromContext(ctx)
|
||||||
|
|
||||||
|
// Add standard parameters
|
||||||
|
params.Set("api_key", c.apiKey)
|
||||||
|
params.Set("format", "json")
|
||||||
|
|
||||||
|
// Construct URL
|
||||||
|
reqUrl, _ := url.Parse(c.baseUrl)
|
||||||
|
reqUrl.RawQuery = params.Encode()
|
||||||
|
|
||||||
|
l.Debug().Msgf("Sending request to LastFM: GET %s", reqUrl.String())
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", reqUrl.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getEntity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debug().Msg("Adding LastFM request to queue")
|
||||||
|
body, err := c.queue(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
l.Err(err).Msg("LastFM request failed")
|
||||||
|
return fmt.Errorf("getEntity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, result)
|
||||||
|
if err != nil {
|
||||||
|
l.Err(err).Msg("Failed to unmarshal LastFM response")
|
||||||
|
return fmt.Errorf("getEntity: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectBestImage picks the largest available image from the LastFM slice
|
||||||
|
func (c *LastFMClient) selectBestImage(images []lastFMImage) string {
|
||||||
|
// Rank preference: mega > extralarge > large > medium > small
|
||||||
|
// Since LastFM usually returns them in order of size, we could take the last one,
|
||||||
|
// but a map lookup is safer against API changes.
|
||||||
|
|
||||||
|
imgMap := make(map[string]string)
|
||||||
|
for _, img := range images {
|
||||||
|
if img.URL != "" {
|
||||||
|
imgMap[img.Size] = img.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if url, ok := imgMap["mega"]; ok {
|
||||||
|
if err := ValidateImageURL(overrideImgSize(url)); err == nil {
|
||||||
|
return overrideImgSize(url)
|
||||||
|
} else {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if url, ok := imgMap["extralarge"]; ok {
|
||||||
|
if err := ValidateImageURL(overrideImgSize(url)); err == nil {
|
||||||
|
return overrideImgSize(url)
|
||||||
|
} else {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if url, ok := imgMap["large"]; ok {
|
||||||
|
if err := ValidateImageURL(overrideImgSize(url)); err == nil {
|
||||||
|
return overrideImgSize(url)
|
||||||
|
} else {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if url, ok := imgMap["medium"]; ok {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
if url, ok := imgMap["small"]; ok {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastfm seems to only return a 300x300 image even for "mega" and "extralarge" images, so I'm cheating
|
||||||
|
func overrideImgSize(url string) string {
|
||||||
|
return strings.Replace(url, "300x300", "600x600", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LastFMClient) GetAlbumImage(ctx context.Context, mbid *uuid.UUID, artist, album string) (string, error) {
|
||||||
|
l := logger.FromContext(ctx)
|
||||||
|
resp := new(lastFMAlbumResponse)
|
||||||
|
l.Debug().Msgf("Finding album image for %s from artist %s", album, artist)
|
||||||
|
|
||||||
|
// Helper to run the fetch
|
||||||
|
fetch := func(query paramsBuilder) error {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("method", "album.getInfo")
|
||||||
|
query(params)
|
||||||
|
return c.getEntity(ctx, params, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Try MBID search first
|
||||||
|
if mbid != nil {
|
||||||
|
l.Debug().Str("mbid", mbid.String()).Msg("Searching album image by MBID")
|
||||||
|
err := fetch(func(p url.Values) {
|
||||||
|
p.Set("mbid", mbid.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
// If success and no API error code
|
||||||
|
if err == nil && resp.Error == 0 && len(resp.Album.Image) > 0 {
|
||||||
|
best := c.selectBestImage(resp.Album.Image)
|
||||||
|
if best != "" {
|
||||||
|
return best, nil
|
||||||
|
}
|
||||||
|
} else if resp.Error != 0 {
|
||||||
|
l.Debug().Int("api_error", resp.Error).Msg("LastFM MBID lookup failed, falling back to name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to Artist + Album name match
|
||||||
|
l.Debug().Str("title", album).Str("artist", artist).Msg("Searching album image by title and artist")
|
||||||
|
|
||||||
|
// Clear previous response structure just in case
|
||||||
|
resp = new(lastFMAlbumResponse)
|
||||||
|
|
||||||
|
err := fetch(func(p url.Values) {
|
||||||
|
p.Set("artist", artist)
|
||||||
|
p.Set("album", album)
|
||||||
|
// Auto-correct spelling is useful for name lookups
|
||||||
|
p.Set("autocorrect", "1")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("GetAlbumImage: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Error != 0 {
|
||||||
|
return "", fmt.Errorf("GetAlbumImage: LastFM API error %d: %s", resp.Error, resp.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
best := c.selectBestImage(resp.Album.Image)
|
||||||
|
if best == "" {
|
||||||
|
return "", fmt.Errorf("GetAlbumImage: no suitable image found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return best, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LastFMClient) GetArtistImage(ctx context.Context, mbid *uuid.UUID, artist string) (string, error) {
|
||||||
|
l := logger.FromContext(ctx)
|
||||||
|
resp := new(lastFMArtistResponse)
|
||||||
|
l.Debug().Msgf("Finding artist image for %s", artist)
|
||||||
|
|
||||||
|
fetch := func(query paramsBuilder) error {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("method", "artist.getInfo")
|
||||||
|
query(params)
|
||||||
|
return c.getEntity(ctx, params, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Try MBID search
|
||||||
|
if mbid != nil {
|
||||||
|
l.Debug().Str("mbid", mbid.String()).Msg("Searching artist image by MBID")
|
||||||
|
err := fetch(func(p url.Values) {
|
||||||
|
p.Set("mbid", mbid.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
if err == nil && resp.Error == 0 && len(resp.Artist.Image) > 0 {
|
||||||
|
best := c.selectBestImage(resp.Artist.Image)
|
||||||
|
if best != "" {
|
||||||
|
// Validate to match Subsonic implementation behavior
|
||||||
|
if err := ValidateImageURL(best); err == nil {
|
||||||
|
return best, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fallback to Artist name
|
||||||
|
l.Debug().Str("artist", artist).Msg("Searching artist image by name")
|
||||||
|
resp = new(lastFMArtistResponse)
|
||||||
|
|
||||||
|
err := fetch(func(p url.Values) {
|
||||||
|
p.Set("artist", artist)
|
||||||
|
p.Set("autocorrect", "1")
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("GetArtistImage: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Error != 0 {
|
||||||
|
return "", fmt.Errorf("GetArtistImage: LastFM API error %d: %s", resp.Error, resp.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
best := c.selectBestImage(resp.Artist.Image)
|
||||||
|
if best == "" {
|
||||||
|
return "", fmt.Errorf("GetArtistImage: no suitable image found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ValidateImageURL(best); err != nil {
|
||||||
|
return "", fmt.Errorf("GetArtistImage: failed to validate image url")
|
||||||
|
}
|
||||||
|
|
||||||
|
return best, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type paramsBuilder func(url.Values)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue