ListenBrainz: Fix love import and rate limit check

This commit is contained in:
Philipp Wolfer 2023-11-13 11:42:09 +01:00
parent 161ada7aff
commit aa453e4dc2
No known key found for this signature in database
GPG key ID: 8FDF744D4919943B
8 changed files with 101 additions and 33 deletions

View file

@ -46,6 +46,7 @@ func NewClient(token string) Client {
client.SetAuthScheme("Token")
client.SetAuthToken(token)
client.SetHeader("Accept", "application/json")
client.SetHeader("Content-Type", "application/json")
// Handle rate limiting (see https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#rate-limiting)
client.SetRetryCount(5)
@ -54,6 +55,7 @@ func NewClient(token string) Client {
return r.StatusCode() == http.StatusTooManyRequests
},
)
client.SetRetryMaxWaitTime(time.Duration(1 * time.Minute))
client.SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) {
resetIn, err := strconv.Atoi(resp.Header().Get("X-RateLimit-Reset-In"))
// fmt.Printf("R %v: %v, %v\n", resp.Request.URL, resetIn, err)
@ -68,6 +70,7 @@ func NewClient(token string) Client {
func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (result GetListensResult, err error) {
const path = "/user/{username}/listens"
errorResult := ErrorResult{}
response, err := c.HttpClient.R().
SetPathParam("username", user).
SetQueryParams(map[string]string{
@ -76,10 +79,11 @@ func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (r
"count": strconv.Itoa(c.MaxResults),
}).
SetResult(&result).
SetError(&errorResult).
Get(path)
if response.StatusCode() != 200 {
err = errors.New(response.String())
err = errors.New(errorResult.Error)
return
}
return
@ -87,6 +91,7 @@ func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (r
func (c Client) GetFeedback(user string, status int, offset int) (result GetFeedbackResult, err error) {
const path = "/feedback/user/{username}/get-feedback"
errorResult := ErrorResult{}
response, err := c.HttpClient.R().
SetPathParam("username", user).
SetQueryParams(map[string]string{
@ -96,10 +101,11 @@ func (c Client) GetFeedback(user string, status int, offset int) (result GetFeed
"metadata": "true",
}).
SetResult(&result).
SetError(&errorResult).
Get(path)
if response.StatusCode() != 200 {
err = errors.New(response.String())
err = errors.New(errorResult.Error)
return
}
return
@ -107,13 +113,15 @@ func (c Client) GetFeedback(user string, status int, offset int) (result GetFeed
func (c Client) SendFeedback(feedback Feedback) (result StatusResult, err error) {
const path = "/feedback/recording-feedback"
errorResult := ErrorResult{}
response, err := c.HttpClient.R().
SetBody(feedback).
SetResult(&result).
SetError(&errorResult).
Post(path)
if response.StatusCode() != 200 {
err = errors.New(response.String())
err = errors.New(errorResult.Error)
return
}
return

View file

@ -22,6 +22,7 @@ THE SOFTWARE.
package listenbrainz
import (
"fmt"
"slices"
"time"
@ -113,6 +114,7 @@ func (b ListenBrainzApiBackend) ImportLoves(loves []models.Love, oldestTimestamp
TotalCount: len(loves),
ImportCount: 0,
LastTimestamp: oldestTimestamp,
ImportErrors: make([]string, 0),
}
for _, love := range loves {
if love.Created.Unix() <= oldestTimestamp.Unix() {
@ -121,16 +123,20 @@ func (b ListenBrainzApiBackend) ImportLoves(loves []models.Love, oldestTimestamp
// TODO: Support love import without recording MBID
if love.RecordingMbid != "" {
_, err := b.client.SendFeedback(Feedback{
resp, err := b.client.SendFeedback(Feedback{
RecordingMbid: string(love.RecordingMbid),
Score: 1,
})
if err != nil {
if err == nil && resp.Status == "ok" {
result.ImportCount += 1
if love.Created.Unix() > result.LastTimestamp.Unix() {
result.LastTimestamp = love.Created
}
} else {
msg := fmt.Sprintf("Failed import of \"%s\" by %s: %v",
love.TrackName, love.ArtistName(), err.Error())
result.ImportErrors = append(result.ImportErrors, msg)
}
}
}
@ -166,17 +172,22 @@ func (f Feedback) ToLove() models.Love {
RecordingMbid: recordingMbid,
Created: time.Unix(f.Created, 0),
Track: models.Track{
TrackName: track.TrackName,
ReleaseName: track.ReleaseName,
ArtistNames: []string{track.ArtistName},
RecordingMbid: recordingMbid,
ReleaseMbid: models.MBID(track.MbidMapping.ReleaseMbid),
ArtistMbids: make([]models.MBID, 0, len(track.MbidMapping.ArtistMbids)),
},
}
for _, artistMbid := range track.MbidMapping.ArtistMbids {
love.Track.ArtistMbids = append(love.Track.ArtistMbids, models.MBID(artistMbid))
if track != nil {
love.Track.TrackName = track.TrackName
love.Track.ReleaseName = track.ReleaseName
love.ArtistNames = []string{track.ArtistName}
love.ReleaseMbid = models.MBID(track.MbidMapping.ReleaseMbid)
love.ArtistMbids = make([]models.MBID, 0, len(track.MbidMapping.ArtistMbids))
if track.MbidMapping != nil {
for _, artistMbid := range track.MbidMapping.ArtistMbids {
love.Track.ArtistMbids = append(love.Track.ArtistMbids, models.MBID(artistMbid))
}
}
}
return love

View file

@ -82,11 +82,11 @@ func TestListenBrainzFeedbackToLove(t *testing.T) {
RecordingMbid: recordingMbid,
Score: 1,
UserName: "ousidecontext",
TrackMetadata: listenbrainz.Track{
TrackMetadata: &listenbrainz.Track{
TrackName: "Oweynagat",
ArtistName: "Dool",
ReleaseName: "Here Now, There Then",
MbidMapping: listenbrainz.MbidMapping{
MbidMapping: &listenbrainz.MbidMapping{
RecordingMbid: recordingMbid,
ReleaseMbid: releaseMbid,
ArtistMbids: []string{artistMbid},
@ -106,3 +106,18 @@ func TestListenBrainzFeedbackToLove(t *testing.T) {
require.Len(t, love.Track.ArtistMbids, 1)
assert.Equal(models.MBID(artistMbid), love.Track.ArtistMbids[0])
}
func TestListenBrainzPartialFeedbackToLove(t *testing.T) {
recordingMbid := "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"
feedback := listenbrainz.Feedback{
Created: 1699859066,
RecordingMbid: recordingMbid,
Score: 1,
}
love := feedback.ToLove()
assert := assert.New(t)
assert.Equal(time.Unix(1699859066, 0).Unix(), love.Created.Unix())
assert.Equal(models.MBID(recordingMbid), love.RecordingMbid)
assert.Equal(models.MBID(recordingMbid), love.Track.RecordingMbid)
assert.Empty(love.Track.TrackName)
}

View file

@ -48,25 +48,25 @@ type Listen struct {
}
type Track struct {
TrackName string `json:"track_name"`
ArtistName string `json:"artist_name"`
ReleaseName string `json:"release_name"`
AdditionalInfo map[string]any `json:"additional_info"`
MbidMapping MbidMapping `json:"mbid_mapping"`
TrackName string `json:"track_name,omitempty"`
ArtistName string `json:"artist_name,omitempty"`
ReleaseName string `json:"release_name,omitempty"`
AdditionalInfo map[string]any `json:"additional_info,omitempty"`
MbidMapping *MbidMapping `json:"mbid_mapping,omitempty"`
}
type MbidMapping struct {
RecordingName string `json:"recording_name"`
RecordingMbid string `json:"recording_mbid"`
ReleaseMbid string `json:"release_mbid"`
ArtistMbids []string `json:"artist_mbids"`
Artists []Artist `json:"artists"`
RecordingName string `json:"recording_name,omitempty"`
RecordingMbid string `json:"recording_mbid,omitempty"`
ReleaseMbid string `json:"release_mbid,omitempty"`
ArtistMbids []string `json:"artist_mbids,omitempty"`
Artists []Artist `json:"artists,omitempty"`
}
type Artist struct {
ArtistCreditName string `json:"artist_credit_name"`
ArtistMbid string `json:"artist_mbid"`
JoinPhrase string `json:"join_phrase"`
ArtistCreditName string `json:"artist_credit_name,omitempty"`
ArtistMbid string `json:"artist_mbid,omitempty"`
JoinPhrase string `json:"join_phrase,omitempty"`
}
type GetFeedbackResult struct {
@ -77,18 +77,23 @@ type GetFeedbackResult struct {
}
type Feedback struct {
Created int64 `json:"created"`
RecordingMbid string `json:"recording_mbid"`
RecordingMsid string `json:"recording_msid"`
Score int `json:"score"`
TrackMetadata Track `json:"track_metadata"`
UserName string `json:"user_id"`
Created int64 `json:"created,omitempty"`
RecordingMbid string `json:"recording_mbid,omitempty"`
RecordingMsid string `json:"recording_msid,omitempty"`
Score int `json:"score,omitempty"`
TrackMetadata *Track `json:"track_metadata,omitempty"`
UserName string `json:"user_id,omitempty"`
}
type StatusResult struct {
Status string `json:"status"`
}
type ErrorResult struct {
Code int `json:"code"`
Error string `json:"error"`
}
func (t Track) Duration() time.Duration {
info := t.AdditionalInfo
millisecondsF, ok := tryGetFloat[float64](info, "duration_ms")

View file

@ -22,10 +22,12 @@ THE SOFTWARE.
package listenbrainz_test
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uploadedlobster.com/scotty/backends/listenbrainz"
)
@ -157,3 +159,15 @@ func TestReleaseGroupMbid(t *testing.T) {
}
assert.Equal(t, expected, track.ReleaseGroupMbid())
}
func TestMarshalPartialFeedback(t *testing.T) {
feedback := listenbrainz.Feedback{
Created: 1699859066,
RecordingMbid: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12",
}
b, err := json.Marshal(feedback)
require.NoError(t, err)
assert.Equal(t,
"{\"created\":1699859066,\"recording_mbid\":\"c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12\"}",
string(b))
}

View file

@ -50,6 +50,13 @@ var listensCmd = &cobra.Command{
cobra.CheckErr(err)
fmt.Printf("Imported %v of %v listens (last timestamp %v)\n",
result.ImportCount, result.TotalCount, result.LastTimestamp)
if len(result.ImportErrors) > 0 {
fmt.Printf("\nDuring the import the following errors occurred:\n")
for _, err := range result.ImportErrors {
fmt.Printf("Error: %v\n", err)
}
}
},
}

View file

@ -50,6 +50,13 @@ var lovesCmd = &cobra.Command{
cobra.CheckErr(err)
fmt.Printf("Imported %v of %v loves (last timestamp %v)\n",
result.ImportCount, result.TotalCount, result.LastTimestamp)
if len(result.ImportErrors) > 0 {
fmt.Printf("\nDuring the import the following errors occurred:\n")
for _, err := range result.ImportErrors {
fmt.Printf("Error: %v\n", err)
}
}
},
}

View file

@ -51,4 +51,5 @@ type ImportResult struct {
TotalCount int
ImportCount int
LastTimestamp time.Time
ImportErrors []string
}