fix: improve subsonic image searching (#164)

This commit is contained in:
Gabe Farrell 2026-01-21 14:54:52 -05:00 committed by GitHub
parent 1a8099e902
commit 56ac73d12b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 53 additions and 12 deletions

View file

@ -363,8 +363,9 @@ func FetchMissingAlbumImages(ctx context.Context, store db.DB) error {
var imgid uuid.UUID
imgUrl, imgErr := images.GetAlbumImage(ctx, images.AlbumImageOpts{
Artists: utils.FlattenSimpleArtistNames(album.Artists),
Album: album.Title,
Artists: utils.FlattenSimpleArtistNames(album.Artists),
Album: album.Title,
ReleaseMbzID: album.MbzID,
})
if imgErr == nil && imgUrl != "" {
imgid = uuid.New()

View file

@ -31,6 +31,7 @@ var imgsrc ImageSource
type ArtistImageOpts struct {
Aliases []string
MBID *uuid.UUID
}
type AlbumImageOpts struct {
@ -66,7 +67,7 @@ func Shutdown() {
func GetArtistImage(ctx context.Context, opts ArtistImageOpts) (string, error) {
l := logger.FromContext(ctx)
if imgsrc.subsonicEnabled {
img, err := imgsrc.subsonicC.GetArtistImage(ctx, opts.Aliases[0])
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 != "" {
@ -92,7 +93,7 @@ func GetArtistImage(ctx context.Context, opts ArtistImageOpts) (string, error) {
func GetAlbumImage(ctx context.Context, opts AlbumImageOpts) (string, error) {
l := logger.FromContext(ctx)
if imgsrc.subsonicEnabled {
img, err := imgsrc.subsonicC.GetAlbumImage(ctx, opts.Artists[0], opts.Album)
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")
}

View file

@ -11,6 +11,7 @@ import (
"github.com/gabehf/koito/internal/cfg"
"github.com/gabehf/koito/internal/logger"
"github.com/gabehf/koito/queue"
"github.com/google/uuid"
)
type SubsonicClient struct {
@ -26,6 +27,8 @@ type SubsonicAlbumResponse struct {
SearchResult3 struct {
Album []struct {
CoverArt string `json:"coverArt"`
Artist string `json:"artist"`
MBID string `json:"musicBrainzId"`
} `json:"album"`
} `json:"searchResult3"`
} `json:"subsonic-response"`
@ -43,7 +46,7 @@ type SubsonicArtistResponse struct {
}
const (
subsonicAlbumSearchFmtStr = "/rest/search3?%s&f=json&query=%s&v=1.13.0&c=koito&artistCount=0&songCount=0&albumCount=1"
subsonicAlbumSearchFmtStr = "/rest/search3?%s&f=json&query=%s&v=1.13.0&c=koito&artistCount=0&songCount=0&albumCount=10"
subsonicArtistSearchFmtStr = "/rest/search3?%s&f=json&query=%s&v=1.13.0&c=koito&artistCount=1&songCount=0&albumCount=0"
subsonicCoverArtFmtStr = "/rest/getCoverArt?%s&id=%s&v=1.13.0&c=koito"
)
@ -106,25 +109,61 @@ func (c *SubsonicClient) getEntity(ctx context.Context, endpoint string, result
return nil
}
func (c *SubsonicClient) GetAlbumImage(ctx context.Context, artist, album string) (string, error) {
func (c *SubsonicClient) GetAlbumImage(ctx context.Context, mbid *uuid.UUID, artist, album string) (string, error) {
l := logger.FromContext(ctx)
resp := new(SubsonicAlbumResponse)
l.Debug().Msgf("Finding album image for %s from artist %s", album, artist)
err := c.getEntity(ctx, fmt.Sprintf(subsonicAlbumSearchFmtStr, c.authParams, url.QueryEscape(artist+" "+album)), resp)
// first try mbid search
if mbid != nil {
l.Debug().Str("mbid", mbid.String()).Msg("Searching album image by MBID")
err := c.getEntity(ctx, fmt.Sprintf(subsonicAlbumSearchFmtStr, c.authParams, url.QueryEscape(mbid.String())), resp)
if err != nil {
return "", fmt.Errorf("GetAlbumImage: %v", err)
}
l.Debug().Any("subsonic_response", resp).Msg("")
if len(resp.SubsonicResponse.SearchResult3.Album) >= 1 {
return cfg.SubsonicUrl() + fmt.Sprintf(subsonicCoverArtFmtStr, c.authParams, url.QueryEscape(resp.SubsonicResponse.SearchResult3.Album[0].CoverArt)), nil
}
}
// else do artist match
l.Debug().Str("title", album).Str("artist", artist).Msg("Searching album image by title and artist")
err := c.getEntity(ctx, fmt.Sprintf(subsonicAlbumSearchFmtStr, c.authParams, url.QueryEscape(album)), resp)
if err != nil {
return "", fmt.Errorf("GetAlbumImage: %v", err)
}
l.Debug().Any("subsonic_response", resp).Send()
if len(resp.SubsonicResponse.SearchResult3.Album) < 1 || resp.SubsonicResponse.SearchResult3.Album[0].CoverArt == "" {
return "", fmt.Errorf("GetAlbumImage: failed to get album art")
l.Debug().Any("subsonic_response", resp).Msg("")
if len(resp.SubsonicResponse.SearchResult3.Album) < 1 {
return "", fmt.Errorf("GetAlbumImage: failed to get album art from subsonic")
}
return cfg.SubsonicUrl() + fmt.Sprintf(subsonicCoverArtFmtStr, c.authParams, url.QueryEscape(resp.SubsonicResponse.SearchResult3.Album[0].CoverArt)), nil
for _, album := range resp.SubsonicResponse.SearchResult3.Album {
if album.Artist == artist {
return cfg.SubsonicUrl() + fmt.Sprintf(subsonicCoverArtFmtStr, c.authParams, url.QueryEscape(resp.SubsonicResponse.SearchResult3.Album[0].CoverArt)), nil
}
}
return "", fmt.Errorf("GetAlbumImage: failed to get album art from subsonic")
}
func (c *SubsonicClient) GetArtistImage(ctx context.Context, artist string) (string, error) {
func (c *SubsonicClient) GetArtistImage(ctx context.Context, mbid *uuid.UUID, artist string) (string, error) {
l := logger.FromContext(ctx)
resp := new(SubsonicArtistResponse)
l.Debug().Msgf("Finding artist image for %s", artist)
// first try mbid search
if mbid != nil {
l.Debug().Str("mbid", mbid.String()).Msg("Searching artist image by MBID")
err := c.getEntity(ctx, fmt.Sprintf(subsonicArtistSearchFmtStr, c.authParams, url.QueryEscape(mbid.String())), resp)
if err != nil {
return "", fmt.Errorf("GetArtistImage: %v", err)
}
l.Debug().Any("subsonic_response", resp).Msg("")
if len(resp.SubsonicResponse.SearchResult3.Artist) < 1 || resp.SubsonicResponse.SearchResult3.Artist[0].ArtistImageUrl == "" {
return "", fmt.Errorf("GetArtistImage: failed to get artist art")
}
// Subsonic seems to have a tendency to return an artist image even though the url is a 404
if err = ValidateImageURL(resp.SubsonicResponse.SearchResult3.Artist[0].ArtistImageUrl); err != nil {
return "", fmt.Errorf("GetArtistImage: failed to get validate image url")
}
}
l.Debug().Str("artist", artist).Msg("Searching artist image by name")
err := c.getEntity(ctx, fmt.Sprintf(subsonicArtistSearchFmtStr, c.authParams, url.QueryEscape(artist)), resp)
if err != nil {
return "", fmt.Errorf("GetArtistImage: %v", err)