mirror of
https://git.sr.ht/~phw/scotty
synced 2025-04-21 20:29:28 +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
|
@ -60,7 +60,7 @@ func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (r
|
||||||
SetQueryParams(map[string]string{
|
SetQueryParams(map[string]string{
|
||||||
"max_ts": strconv.FormatInt(maxTime.Unix(), 10),
|
"max_ts": strconv.FormatInt(maxTime.Unix(), 10),
|
||||||
"min_ts": strconv.FormatInt(minTime.Unix(), 10),
|
"min_ts": strconv.FormatInt(minTime.Unix(), 10),
|
||||||
"count": strconv.FormatInt(int64(c.MaxResults), 10),
|
"count": strconv.Itoa(c.MaxResults),
|
||||||
}).
|
}).
|
||||||
SetResult(&result).
|
SetResult(&result).
|
||||||
Get(path)
|
Get(path)
|
||||||
|
@ -71,3 +71,36 @@ func (c Client) GetListens(user string, maxTime time.Time, minTime time.Time) (r
|
||||||
}
|
}
|
||||||
return
|
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) {
|
func TestGetListens(t *testing.T) {
|
||||||
defer httpmock.DeactivateAndReset()
|
defer httpmock.DeactivateAndReset()
|
||||||
|
|
||||||
token := "thetoken"
|
client := listenbrainz.NewClient("thetoken")
|
||||||
client := listenbrainz.NewClient(token)
|
client.MaxResults = 2
|
||||||
setupHttpMock(t, client.HttpClient.GetClient(),
|
setupHttpMock(t, client.HttpClient.GetClient(),
|
||||||
"https://api.listenbrainz.org/1/user/outsidecontext/listens",
|
"https://api.listenbrainz.org/1/user/outsidecontext/listens",
|
||||||
"testdata/listens.json")
|
"testdata/listens.json")
|
||||||
|
@ -57,6 +57,49 @@ func TestGetListens(t *testing.T) {
|
||||||
assert.Equal("Shadowplay", result.Payload.Listens[0].TrackMetadata.TrackName)
|
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) {
|
func setupHttpMock(t *testing.T, client *http.Client, url string, testDataPath string) {
|
||||||
httpmock.ActivateNonDefault(client)
|
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) {
|
func (b ListenBrainzApiBackend) ExportListens(oldestTimestamp time.Time) ([]models.Listen, error) {
|
||||||
maxTime := time.Now()
|
maxTime := time.Now()
|
||||||
minTime := time.Unix(0, 0)
|
minTime := time.Unix(0, 0)
|
||||||
listens := make([]models.Listen, 0)
|
listens := make([]models.Listen, 0, 2*MaxItemsPerGet)
|
||||||
|
|
||||||
out:
|
out:
|
||||||
for {
|
for {
|
||||||
|
@ -76,6 +76,69 @@ out:
|
||||||
return listens, nil
|
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 {
|
func (lbListen Listen) ToListen() models.Listen {
|
||||||
track := lbListen.TrackMetadata
|
track := lbListen.TrackMetadata
|
||||||
listen := models.Listen{
|
listen := models.Listen{
|
||||||
|
@ -96,3 +159,27 @@ func (lbListen Listen) ToListen() models.Listen {
|
||||||
}
|
}
|
||||||
return 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/spf13/viper"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"go.uploadedlobster.com/scotty/backends/listenbrainz"
|
"go.uploadedlobster.com/scotty/backends/listenbrainz"
|
||||||
"go.uploadedlobster.com/scotty/models"
|
"go.uploadedlobster.com/scotty/models"
|
||||||
)
|
)
|
||||||
|
@ -71,3 +72,37 @@ func TestListenBrainzListenToListen(t *testing.T) {
|
||||||
assert.Equal(t, "DES561620801", listen.Isrc)
|
assert.Equal(t, "DES561620801", listen.Isrc)
|
||||||
assert.Equal(t, lbListen.TrackMetadata.AdditionalInfo["foo"], listen.AdditionalInfo["foo"])
|
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"`
|
ArtistName string `json:"artist_name"`
|
||||||
ReleaseName string `json:"release_name"`
|
ReleaseName string `json:"release_name"`
|
||||||
AdditionalInfo map[string]any `json:"additional_info"`
|
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 {
|
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