mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-21 12:19:29 +02:00
ListenBrainz: Love export and basic import
Love import currently works only for tracks with existing recording MBID
This commit is contained in:
parent
ead323eaed
commit
0020594ea3
6 changed files with 300 additions and 4 deletions
backends/listenbrainz
|
@ -60,7 +60,7 @@ func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (r
|
|||
SetQueryParams(map[string]string{
|
||||
"max_ts": strconv.FormatInt(maxTime.Unix(), 10),
|
||||
"min_ts": strconv.FormatInt(minTime.Unix(), 10),
|
||||
"count": strconv.FormatInt(int64(c.MaxResults), 10),
|
||||
"count": strconv.Itoa(c.MaxResults),
|
||||
}).
|
||||
SetResult(&result).
|
||||
Get(path)
|
||||
|
@ -71,3 +71,36 @@ func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (r
|
|||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) GetFeedback(user string, status int, offset int) (result GetFeedbackResult, err error) {
|
||||
const path = "/feedback/user/{username}/get-feedback"
|
||||
response, err := c.HttpClient.R().
|
||||
SetPathParam("username", user).
|
||||
SetQueryParams(map[string]string{
|
||||
"status": strconv.Itoa(status),
|
||||
"offset": strconv.Itoa(offset),
|
||||
"count": strconv.Itoa(c.MaxResults),
|
||||
}).
|
||||
SetResult(&result).
|
||||
Get(path)
|
||||
|
||||
if response.StatusCode() != 200 {
|
||||
err = errors.New(response.String())
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c Client) SendFeedback(feedback Feedback) (result StatusResult, err error) {
|
||||
const path = "/feedback/recording-feedback"
|
||||
response, err := c.HttpClient.R().
|
||||
SetBody(feedback).
|
||||
SetResult(&result).
|
||||
Post(path)
|
||||
|
||||
if response.StatusCode() != 200 {
|
||||
err = errors.New(response.String())
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
|
@ -42,8 +42,8 @@ func TestNewClient(t *testing.T) {
|
|||
func TestGetListens(t *testing.T) {
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
token := "thetoken"
|
||||
client := listenbrainz.NewClient(token)
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
client.MaxResults = 2
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
"https://api.listenbrainz.org/1/user/outsidecontext/listens",
|
||||
"testdata/listens.json")
|
||||
|
@ -57,6 +57,49 @@ func TestGetListens(t *testing.T) {
|
|||
assert.Equal("Shadowplay", result.Payload.Listens[0].TrackMetadata.TrackName)
|
||||
}
|
||||
|
||||
func TestGetFeedback(t *testing.T) {
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
client.MaxResults = 2
|
||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||
"https://api.listenbrainz.org/1/feedback/user/outsidecontext/get-feedback",
|
||||
"testdata/feedback.json")
|
||||
|
||||
result, err := client.GetFeedback("outsidecontext", 1, 3)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert := assert.New(t)
|
||||
assert.Equal(2, result.Count)
|
||||
assert.Equal(302, result.TotalCount)
|
||||
assert.Equal(3, result.Offset)
|
||||
require.Len(t, result.Feedback, 2)
|
||||
assert.Equal("c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12", result.Feedback[0].RecordingMbid)
|
||||
}
|
||||
|
||||
func TestSendFeedback(t *testing.T) {
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
httpmock.ActivateNonDefault(client.HttpClient.GetClient())
|
||||
|
||||
responder, err := httpmock.NewJsonResponder(200, listenbrainz.StatusResult{
|
||||
Status: "ok",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
url := "https://api.listenbrainz.org/1/feedback/recording-feedback"
|
||||
httpmock.RegisterResponder("POST", url, responder)
|
||||
|
||||
feedback := listenbrainz.Feedback{
|
||||
RecordingMbid: "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12",
|
||||
Score: 1,
|
||||
}
|
||||
result, err := client.SendFeedback(feedback)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "ok", result.Status)
|
||||
}
|
||||
|
||||
func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||
httpmock.ActivateNonDefault(client)
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ func (b ListenBrainzApiBackend) FromConfig(config *viper.Viper) models.Backend {
|
|||
func (b ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time) ([]models.Listen, error) {
|
||||
maxTime := time.Now()
|
||||
minTime := time.Unix(0, 0)
|
||||
listens := make([]models.Listen, 0)
|
||||
listens := make([]models.Listen, 0, 2*MaxItemsPerGet)
|
||||
|
||||
out:
|
||||
for {
|
||||
|
@ -76,6 +76,69 @@ out:
|
|||
return listens, nil
|
||||
}
|
||||
|
||||
func (b ListenBrainzApiBackend) ExportLoves(oldestTimestamp time.Time) ([]models.Love, error) {
|
||||
offset := 0
|
||||
// perPage := MaxItemsPerGet
|
||||
|
||||
loves := make([]models.Love, 0, 2*MaxItemsPerGet)
|
||||
|
||||
out:
|
||||
for {
|
||||
result, err := b.client.GetFeedback(b.username, 1, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
count := len(result.Feedback)
|
||||
if count == 0 {
|
||||
break out
|
||||
}
|
||||
|
||||
for _, feedback := range result.Feedback {
|
||||
love := feedback.ToLove()
|
||||
if love.Created.Unix() > oldestTimestamp.Unix() {
|
||||
loves = append(loves, love)
|
||||
} else {
|
||||
break out
|
||||
}
|
||||
}
|
||||
|
||||
offset += 1
|
||||
}
|
||||
|
||||
slices.Reverse(loves)
|
||||
return loves, nil
|
||||
}
|
||||
|
||||
func (b ListenBrainzApiBackend) ImportLoves(loves []models.Love, oldestTimestamp time.Time) (models.ImportResult, error) {
|
||||
result := models.ImportResult{
|
||||
TotalCount: len(loves),
|
||||
ImportCount: 0,
|
||||
LastTimestamp: oldestTimestamp,
|
||||
}
|
||||
for _, love := range loves {
|
||||
if love.Created.Unix() <= oldestTimestamp.Unix() {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: Support love import without recording MBID
|
||||
if love.RecordingMbid != "" {
|
||||
_, err := b.client.SendFeedback(Feedback{
|
||||
RecordingMbid: string(love.RecordingMbid),
|
||||
Score: 1,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
result.ImportCount += 1
|
||||
if love.Created.Unix() > result.LastTimestamp.Unix() {
|
||||
result.LastTimestamp = love.Created
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (lbListen Listen) ToListen() models.Listen {
|
||||
track := lbListen.TrackMetadata
|
||||
listen := models.Listen{
|
||||
|
@ -96,3 +159,27 @@ func (lbListen Listen) ToListen() models.Listen {
|
|||
}
|
||||
return listen
|
||||
}
|
||||
|
||||
func (f Feedback) ToLove() models.Love {
|
||||
track := f.TrackMetadata
|
||||
recordingMbid := models.MBID(f.RecordingMbid)
|
||||
love := models.Love{
|
||||
UserName: f.UserName,
|
||||
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))
|
||||
}
|
||||
|
||||
return love
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uploadedlobster.com/scotty/backends/listenbrainz"
|
||||
"go.uploadedlobster.com/scotty/models"
|
||||
)
|
||||
|
@ -71,3 +72,37 @@ func TestListenBrainzListenToListen(t *testing.T) {
|
|||
assert.Equal(t, "DES561620801", listen.Isrc)
|
||||
assert.Equal(t, lbListen.TrackMetadata.AdditionalInfo["foo"], listen.AdditionalInfo["foo"])
|
||||
}
|
||||
|
||||
func TestListenBrainzFeedbackToLove(t *testing.T) {
|
||||
recordingMbid := "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12"
|
||||
releaseMbid := "d7f22677-9803-4d21-ba42-081b633a6f68"
|
||||
artistMbid := "d7f22677-9803-4d21-ba42-081b633a6f68"
|
||||
feedback := listenbrainz.Feedback{
|
||||
Created: 1699859066,
|
||||
RecordingMbid: recordingMbid,
|
||||
Score: 1,
|
||||
UserName: "ousidecontext",
|
||||
TrackMetadata: listenbrainz.Track{
|
||||
TrackName: "Oweynagat",
|
||||
ArtistName: "Dool",
|
||||
ReleaseName: "Here Now, There Then",
|
||||
MbidMapping: listenbrainz.MbidMapping{
|
||||
RecordingMbid: recordingMbid,
|
||||
ReleaseMbid: releaseMbid,
|
||||
ArtistMbids: []string{artistMbid},
|
||||
},
|
||||
},
|
||||
}
|
||||
love := feedback.ToLove()
|
||||
assert := assert.New(t)
|
||||
assert.Equal(time.Unix(1699859066, 0).Unix(), love.Created.Unix())
|
||||
assert.Equal(feedback.UserName, love.UserName)
|
||||
assert.Equal(feedback.TrackMetadata.TrackName, love.TrackName)
|
||||
assert.Equal(feedback.TrackMetadata.ReleaseName, love.ReleaseName)
|
||||
assert.Equal([]string{feedback.TrackMetadata.ArtistName}, love.ArtistNames)
|
||||
assert.Equal(models.MBID(recordingMbid), love.RecordingMbid)
|
||||
assert.Equal(models.MBID(recordingMbid), love.Track.RecordingMbid)
|
||||
assert.Equal(models.MBID(releaseMbid), love.Track.ReleaseMbid)
|
||||
require.Len(t, love.Track.ArtistMbids, 1)
|
||||
assert.Equal(models.MBID(artistMbid), love.Track.ArtistMbids[0])
|
||||
}
|
||||
|
|
|
@ -52,6 +52,41 @@ type Track struct {
|
|||
ArtistName string `json:"artist_name"`
|
||||
ReleaseName string `json:"release_name"`
|
||||
AdditionalInfo map[string]any `json:"additional_info"`
|
||||
MbidMapping MbidMapping `json:"mbid_mapping"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type Artist struct {
|
||||
ArtistCreditName string `json:"artist_credit_name"`
|
||||
ArtistMbid string `json:"artist_mbid"`
|
||||
JoinPhrase string `json:"join_phrase"`
|
||||
}
|
||||
|
||||
type GetFeedbackResult struct {
|
||||
Count int `json:"count"`
|
||||
TotalCount int `json:"total_count"`
|
||||
Offset int `json:"offset"`
|
||||
Feedback []Feedback `json:"feedback"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type StatusResult struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (t Track) Duration() time.Duration {
|
||||
|
|
63
backends/listenbrainz/testdata/feedback.json
vendored
Normal file
63
backends/listenbrainz/testdata/feedback.json
vendored
Normal file
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"count": 2,
|
||||
"feedback": [
|
||||
{
|
||||
"created": 1699859066,
|
||||
"recording_mbid": "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12",
|
||||
"recording_msid": null,
|
||||
"score": 1,
|
||||
"track_metadata": {
|
||||
"artist_name": "Dool",
|
||||
"mbid_mapping": {
|
||||
"artist_mbids": [
|
||||
"24412926-c7bd-48e8-afad-8a285b42e131"
|
||||
],
|
||||
"artists": [
|
||||
{
|
||||
"artist_credit_name": "Dool",
|
||||
"artist_mbid": "24412926-c7bd-48e8-afad-8a285b42e131",
|
||||
"join_phrase": ""
|
||||
}
|
||||
],
|
||||
"caa_id": 15991300316,
|
||||
"caa_release_mbid": "d7f22677-9803-4d21-ba42-081b633a6f68",
|
||||
"recording_mbid": "c0a1fc94-5f04-4a5f-bc09-e5de0c49cd12",
|
||||
"release_mbid": "aa1ea1ac-7ec4-4542-a494-105afbfe547d"
|
||||
},
|
||||
"release_name": "Here Now, There Then",
|
||||
"track_name": "Oweynagat"
|
||||
},
|
||||
"user_id": "outsidecontext"
|
||||
},
|
||||
{
|
||||
"created": 1698911509,
|
||||
"recording_mbid": "ba49cada-9873-4bdb-9506-533cb63372c8",
|
||||
"recording_msid": null,
|
||||
"score": 1,
|
||||
"track_metadata": {
|
||||
"artist_name": "Hazeshuttle",
|
||||
"mbid_mapping": {
|
||||
"artist_mbids": [
|
||||
"54292079-790c-4e99-bf8d-12efa29fa3e9"
|
||||
],
|
||||
"artists": [
|
||||
{
|
||||
"artist_credit_name": "Hazeshuttle",
|
||||
"artist_mbid": "54292079-790c-4e99-bf8d-12efa29fa3e9",
|
||||
"join_phrase": ""
|
||||
}
|
||||
],
|
||||
"caa_id": 35325252352,
|
||||
"caa_release_mbid": "6d0ee27f-dc9f-4dab-8d7d-f4dcd14dc54a",
|
||||
"recording_mbid": "ba49cada-9873-4bdb-9506-533cb63372c8",
|
||||
"release_mbid": "6d0ee27f-dc9f-4dab-8d7d-f4dcd14dc54a"
|
||||
},
|
||||
"release_name": "Hazeshuttle",
|
||||
"track_name": "Homosativa"
|
||||
},
|
||||
"user_id": "outsidecontext"
|
||||
}
|
||||
],
|
||||
"offset": 3,
|
||||
"total_count": 302
|
||||
}
|
Loading…
Add table
Reference in a new issue