feat: top releases sorted by popularity with library-style album cards

Replaced the date-sorted release group approach with a dedicated
TopReleaseGroupsForArtist LB API call that returns releases ranked
by total listen count (popularity). Added LBTopReleaseGroup type
and Wails bindings.

Restyled the top-releases cards to match the library album view:
square cover art on top with title and type below, centered text,
auto-filling the available width with even spacing.
This commit is contained in:
2026-03-29 17:12:30 -04:00
parent 4138fe593d
commit cef6709d9a
7 changed files with 180 additions and 39 deletions
+5
View File
@@ -184,6 +184,11 @@ func (e *Service) TopRecordingsForArtist(artistMBID string) ([]LBTopRecording, e
return e.lb.TopRecordingsForArtist(e.ctx, artistMBID)
}
// TopReleaseGroupsForArtist returns the most-listened release groups for an artist.
func (e *Service) TopReleaseGroupsForArtist(artistMBID string) ([]LBTopReleaseGroup, error) {
return e.lb.TopReleaseGroupsForArtist(e.ctx, artistMBID)
}
// SimilarArtists returns artists similar to the given artist MBID.
func (e *Service) SimilarArtists(artistMBID string) ([]LBSimilarArtist, error) {
return e.lb.SimilarArtists(e.ctx, artistMBID)
+46
View File
@@ -97,6 +97,52 @@ func (c *ListenBrainzClient) TopRecordingsForArtist(
return out, nil
}
// TopReleaseGroupsForArtist returns the most-listened release groups
// for the artist identified by artistMBID.
func (c *ListenBrainzClient) TopReleaseGroupsForArtist(
ctx context.Context, artistMBID string,
) ([]LBTopReleaseGroup, error) {
url := fmt.Sprintf(
"%s/1/popularity/top-release-groups-for-artist/%s",
listenBrainzBaseURL,
artistMBID,
)
cacheKey := "lb:top-release-groups:" + artistMBID
if data, ok := c.cache.Get(cacheKey); ok {
var out []LBTopReleaseGroup
if err := json.Unmarshal(data, &out); err == nil {
return out, nil
}
}
body, err := c.doGet(ctx, url)
if err != nil {
return nil, fmt.Errorf("listenbrainz top release groups: %w", err)
}
var wire []lbTopReleaseGroupWire
if err := json.Unmarshal(body, &wire); err != nil {
return nil, fmt.Errorf("listenbrainz top release groups unmarshal: %w", err)
}
const maxTopReleaseGroups = 10
limit := len(wire)
if limit > maxTopReleaseGroups {
limit = maxTopReleaseGroups
}
out := make([]LBTopReleaseGroup, limit)
for i := range limit {
out[i] = wire[i].toPublic()
}
c.cacheJSON(cacheKey, out, cacheTTLSearch, artistMBID, "artist")
return out, nil
}
// SimilarArtists returns artists similar to the one identified by
// artistMBID, using the ListenBrainz labs API. Returns nil, nil
// if the endpoint is unavailable (labs API may be unstable).
+42
View File
@@ -106,3 +106,45 @@ type LBSimilarArtist struct {
Name string `json:"name"`
Score float64 `json:"score"`
}
// LBTopReleaseGroup represents a popular release group from the
// ListenBrainz popularity API.
type LBTopReleaseGroup struct {
ReleaseGroupMBID string `json:"releaseGroupMbid"`
Title string `json:"title"`
ArtistName string `json:"artistName"`
Type string `json:"type"`
TotalListenCount int `json:"totalListenCount"`
}
// lbTopReleaseGroupWire matches the ListenBrainz API's snake_case
// JSON response for the popularity/top-release-groups-for-artist
// endpoint.
type lbTopReleaseGroupWire struct {
ReleaseGroupMBID string `json:"release_group_mbid"`
TotalListenCount int `json:"total_listen_count"`
ReleaseGroup struct {
Name string `json:"name"`
Type string `json:"type"`
} `json:"release_group"`
Artist struct {
Artists []struct {
Name string `json:"name"`
} `json:"artists"`
} `json:"artist"`
}
func (w lbTopReleaseGroupWire) toPublic() LBTopReleaseGroup {
artistName := ""
if len(w.Artist.Artists) > 0 {
artistName = w.Artist.Artists[0].Name
}
return LBTopReleaseGroup{
ReleaseGroupMBID: w.ReleaseGroupMBID,
Title: w.ReleaseGroup.Name,
ArtistName: artistName,
Type: w.ReleaseGroup.Type,
TotalListenCount: w.TotalListenCount,
}
}