From eabcf8395e18cbf342636a01de0bbecf3ec79be7 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Tue, 24 Mar 2026 19:35:34 -0400 Subject: [PATCH] fix: unmarshal LB top recordings from snake_case wire format, cap at 10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ListenBrainz popularity API returns snake_case JSON fields (recording_name, artist_name, total_listen_count, recording_mbid) but LBTopRecording used camelCase JSON tags for Wails serialization. All fields silently deserialized as zero values — empty strings and zero counts — producing ~8000 blank rows in the top tracks section. Fix: add lbTopRecordingWire with snake_case tags for API unmarshal, convert to LBTopRecording (camelCase) for Wails. Cap results at 10 to avoid rendering thousands of rows for prolific artists. --- backend/explore/listenbrainz.go | 19 ++++++++++++++++--- backend/explore/types.go | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/backend/explore/listenbrainz.go b/backend/explore/listenbrainz.go index 498f63f..763967b 100644 --- a/backend/explore/listenbrainz.go +++ b/backend/explore/listenbrainz.go @@ -68,12 +68,25 @@ func (c *ListenBrainzClient) TopRecordingsForArtist( return nil, fmt.Errorf("listenbrainz top recordings: %w", err) } - // The API returns an array directly. - var out []LBTopRecording - if err := json.Unmarshal(body, &out); err != nil { + // The API returns snake_case JSON — unmarshal into wire type, + // then convert to the camelCase Wails type. + var wire []lbTopRecordingWire + if err := json.Unmarshal(body, &wire); err != nil { return nil, fmt.Errorf("listenbrainz top recordings unmarshal: %w", err) } + const maxTopRecordings = 10 + + limit := len(wire) + if limit > maxTopRecordings { + limit = maxTopRecordings + } + + out := make([]LBTopRecording, limit) + for i := range limit { + out[i] = wire[i].toPublic() + } + c.cacheJSON(cacheKey, out, cacheTTLSearch, artistMBID, "artist") return out, nil diff --git a/backend/explore/types.go b/backend/explore/types.go index b660a50..71d663a 100644 --- a/backend/explore/types.go +++ b/backend/explore/types.go @@ -68,6 +68,10 @@ type MBTrack struct { // LBTopRecording represents a popular recording from the // ListenBrainz popularity API. +// +// JSON tags use camelCase for Wails→frontend serialization. +// The API response uses snake_case, so we unmarshal into +// lbTopRecordingWire first, then convert. type LBTopRecording struct { RecordingMBID string `json:"recordingMbid"` ArtistName string `json:"artistName"` @@ -75,6 +79,25 @@ type LBTopRecording struct { TotalListenCount int `json:"totalListenCount"` } +// lbTopRecordingWire matches the ListenBrainz API's snake_case +// JSON response for the popularity/top-recordings-for-artist +// endpoint. +type lbTopRecordingWire struct { + RecordingMBID string `json:"recording_mbid"` + ArtistName string `json:"artist_name"` + RecordingName string `json:"recording_name"` + TotalListenCount int `json:"total_listen_count"` +} + +func (w lbTopRecordingWire) toPublic() LBTopRecording { + return LBTopRecording{ + RecordingMBID: w.RecordingMBID, + ArtistName: w.ArtistName, + TrackName: w.RecordingName, + TotalListenCount: w.TotalListenCount, + } +} + // LBSimilarArtist represents a similar artist from the // ListenBrainz labs API. type LBSimilarArtist struct {