mirror of
https://git.sr.ht/~phw/scotty
synced 2025-05-31 10:58:35 +02:00
Moved general LB related code to separate package
This commit is contained in:
parent
34b6bb9aa3
commit
5c56e480f1
10 changed files with 54 additions and 49 deletions
|
@ -29,10 +29,11 @@ import (
|
|||
"go.uploadedlobster.com/scotty/internal/models"
|
||||
"go.uploadedlobster.com/scotty/internal/similarity"
|
||||
"go.uploadedlobster.com/scotty/internal/version"
|
||||
"go.uploadedlobster.com/scotty/pkg/listenbrainz"
|
||||
)
|
||||
|
||||
type ListenBrainzApiBackend struct {
|
||||
client Client
|
||||
client listenbrainz.Client
|
||||
mbClient musicbrainzws2.Client
|
||||
username string
|
||||
checkDuplicates bool
|
||||
|
@ -58,13 +59,13 @@ func (b *ListenBrainzApiBackend) Options() []models.BackendOption {
|
|||
}
|
||||
|
||||
func (b *ListenBrainzApiBackend) InitConfig(config *config.ServiceConfig) error {
|
||||
b.client = NewClient(config.GetString("token"))
|
||||
b.client = listenbrainz.NewClient(config.GetString("token"), version.UserAgent())
|
||||
b.mbClient = *musicbrainzws2.NewClient(musicbrainzws2.AppInfo{
|
||||
Name: version.AppName,
|
||||
Version: version.AppVersion,
|
||||
URL: version.AppURL,
|
||||
})
|
||||
b.client.MaxResults = MaxItemsPerGet
|
||||
b.client.MaxResults = listenbrainz.MaxItemsPerGet
|
||||
b.username = config.GetString("username")
|
||||
b.checkDuplicates = config.GetBool("check-duplicate-listens", false)
|
||||
return nil
|
||||
|
@ -116,7 +117,7 @@ func (b *ListenBrainzApiBackend) ExportListens(ctx context.Context, oldestTimest
|
|||
|
||||
for _, listen := range result.Payload.Listens {
|
||||
if listen.ListenedAt > oldestTimestamp.Unix() {
|
||||
listens = append(listens, listen.AsListen())
|
||||
listens = append(listens, AsListen(listen))
|
||||
} else {
|
||||
// result contains listens older then oldestTimestamp
|
||||
break
|
||||
|
@ -138,16 +139,16 @@ func (b *ListenBrainzApiBackend) ExportListens(ctx context.Context, oldestTimest
|
|||
func (b *ListenBrainzApiBackend) ImportListens(ctx context.Context, export models.ListensResult, importResult models.ImportResult, progress chan models.TransferProgress) (models.ImportResult, error) {
|
||||
total := len(export.Items)
|
||||
p := models.TransferProgress{}.FromImportResult(importResult, false)
|
||||
for i := 0; i < total; i += MaxListensPerRequest {
|
||||
listens := export.Items[i:min(i+MaxListensPerRequest, total)]
|
||||
for i := 0; i < total; i += listenbrainz.MaxListensPerRequest {
|
||||
listens := export.Items[i:min(i+listenbrainz.MaxListensPerRequest, total)]
|
||||
count := len(listens)
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
submission := ListenSubmission{
|
||||
ListenType: Import,
|
||||
Payload: make([]Listen, 0, count),
|
||||
submission := listenbrainz.ListenSubmission{
|
||||
ListenType: listenbrainz.Import,
|
||||
Payload: make([]listenbrainz.Listen, 0, count),
|
||||
}
|
||||
|
||||
for _, l := range listens {
|
||||
|
@ -167,9 +168,9 @@ func (b *ListenBrainzApiBackend) ImportListens(ctx context.Context, export model
|
|||
}
|
||||
|
||||
l.FillAdditionalInfo()
|
||||
listen := Listen{
|
||||
listen := listenbrainz.Listen{
|
||||
ListenedAt: l.ListenedAt.Unix(),
|
||||
TrackMetadata: Track{
|
||||
TrackMetadata: listenbrainz.Track{
|
||||
TrackName: l.TrackName,
|
||||
ReleaseName: l.ReleaseName,
|
||||
ArtistName: l.ArtistName(),
|
||||
|
@ -228,7 +229,7 @@ func (b *ListenBrainzApiBackend) ExportLoves(ctx context.Context, oldestTimestam
|
|||
func (b *ListenBrainzApiBackend) exportLoves(ctx context.Context, oldestTimestamp time.Time, results chan models.LovesResult) {
|
||||
offset := 0
|
||||
defer close(results)
|
||||
loves := make(models.LovesList, 0, 2*MaxItemsPerGet)
|
||||
loves := make(models.LovesList, 0, 2*listenbrainz.MaxItemsPerGet)
|
||||
|
||||
out:
|
||||
for {
|
||||
|
@ -254,7 +255,7 @@ out:
|
|||
}
|
||||
}
|
||||
|
||||
love := feedback.AsLove()
|
||||
love := AsLove(feedback)
|
||||
if love.Created.After(oldestTimestamp) {
|
||||
loves = append(loves, love)
|
||||
} else {
|
||||
|
@ -262,7 +263,7 @@ out:
|
|||
}
|
||||
}
|
||||
|
||||
offset += MaxItemsPerGet
|
||||
offset += listenbrainz.MaxItemsPerGet
|
||||
}
|
||||
|
||||
sort.Sort(loves)
|
||||
|
@ -278,7 +279,7 @@ func (b *ListenBrainzApiBackend) ImportLoves(ctx context.Context, export models.
|
|||
go b.exportLoves(ctx, time.Unix(0, 0), existingLovesChan)
|
||||
|
||||
// TODO: Store MBIDs directly
|
||||
b.existingMBIDs = make(map[mbtypes.MBID]bool, MaxItemsPerGet)
|
||||
b.existingMBIDs = make(map[mbtypes.MBID]bool, listenbrainz.MaxItemsPerGet)
|
||||
|
||||
for existingLoves := range existingLovesChan {
|
||||
if existingLoves.Error != nil {
|
||||
|
@ -316,7 +317,7 @@ func (b *ListenBrainzApiBackend) ImportLoves(ctx context.Context, export models.
|
|||
if b.existingMBIDs[recordingMBID] {
|
||||
ok = true
|
||||
} else {
|
||||
resp, err := b.client.SendFeedback(ctx, Feedback{
|
||||
resp, err := b.client.SendFeedback(ctx, listenbrainz.Feedback{
|
||||
RecordingMBID: recordingMBID,
|
||||
Score: 1,
|
||||
})
|
||||
|
@ -366,7 +367,7 @@ func (b *ListenBrainzApiBackend) checkDuplicateListen(ctx context.Context, liste
|
|||
}
|
||||
|
||||
for _, c := range candidates.Payload.Listens {
|
||||
sim := similarity.CompareTracks(listen.Track, c.TrackMetadata.AsTrack())
|
||||
sim := similarity.CompareTracks(listen.Track, AsTrack(c.TrackMetadata))
|
||||
if sim >= trackSimilarityThreshold {
|
||||
return true, nil
|
||||
}
|
||||
|
@ -375,7 +376,8 @@ func (b *ListenBrainzApiBackend) checkDuplicateListen(ctx context.Context, liste
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (b *ListenBrainzApiBackend) lookupRecording(ctx context.Context, mbid mbtypes.MBID) (*Track, error) {
|
||||
func (b *ListenBrainzApiBackend) lookupRecording(
|
||||
ctx context.Context, mbid mbtypes.MBID) (*listenbrainz.Track, error) {
|
||||
filter := musicbrainzws2.IncludesFilter{
|
||||
Includes: []string{"artist-credits"},
|
||||
}
|
||||
|
@ -388,10 +390,10 @@ func (b *ListenBrainzApiBackend) lookupRecording(ctx context.Context, mbid mbtyp
|
|||
for _, artist := range recording.ArtistCredit {
|
||||
artistMBIDs = append(artistMBIDs, artist.Artist.ID)
|
||||
}
|
||||
track := Track{
|
||||
track := listenbrainz.Track{
|
||||
TrackName: recording.Title,
|
||||
ArtistName: recording.ArtistCredit.String(),
|
||||
MBIDMapping: &MBIDMapping{
|
||||
MBIDMapping: &listenbrainz.MBIDMapping{
|
||||
// In case of redirects this MBID differs from the looked up MBID
|
||||
RecordingMBID: recording.ID,
|
||||
ArtistMBIDs: artistMBIDs,
|
||||
|
@ -400,26 +402,26 @@ func (b *ListenBrainzApiBackend) lookupRecording(ctx context.Context, mbid mbtyp
|
|||
return &track, nil
|
||||
}
|
||||
|
||||
func (lbListen Listen) AsListen() models.Listen {
|
||||
func AsListen(lbListen listenbrainz.Listen) models.Listen {
|
||||
listen := models.Listen{
|
||||
ListenedAt: time.Unix(lbListen.ListenedAt, 0),
|
||||
UserName: lbListen.UserName,
|
||||
Track: lbListen.TrackMetadata.AsTrack(),
|
||||
Track: AsTrack(lbListen.TrackMetadata),
|
||||
}
|
||||
return listen
|
||||
}
|
||||
|
||||
func (f Feedback) AsLove() models.Love {
|
||||
func AsLove(f listenbrainz.Feedback) models.Love {
|
||||
recordingMBID := f.RecordingMBID
|
||||
track := f.TrackMetadata
|
||||
if track == nil {
|
||||
track = &Track{}
|
||||
track = &listenbrainz.Track{}
|
||||
}
|
||||
love := models.Love{
|
||||
UserName: f.UserName,
|
||||
RecordingMBID: recordingMBID,
|
||||
Created: time.Unix(f.Created, 0),
|
||||
Track: track.AsTrack(),
|
||||
Track: AsTrack(*track),
|
||||
}
|
||||
|
||||
if love.Track.RecordingMBID == "" {
|
||||
|
@ -429,7 +431,7 @@ func (f Feedback) AsLove() models.Love {
|
|||
return love
|
||||
}
|
||||
|
||||
func (t Track) AsTrack() models.Track {
|
||||
func AsTrack(t listenbrainz.Track) models.Track {
|
||||
track := models.Track{
|
||||
TrackName: t.TrackName,
|
||||
ReleaseName: t.ReleaseName,
|
||||
|
|
|
@ -24,15 +24,16 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uploadedlobster.com/mbtypes"
|
||||
"go.uploadedlobster.com/scotty/internal/backends/listenbrainz"
|
||||
lbapi "go.uploadedlobster.com/scotty/internal/backends/listenbrainz"
|
||||
"go.uploadedlobster.com/scotty/internal/config"
|
||||
"go.uploadedlobster.com/scotty/pkg/listenbrainz"
|
||||
)
|
||||
|
||||
func TestInitConfig(t *testing.T) {
|
||||
c := viper.New()
|
||||
c.Set("token", "thetoken")
|
||||
service := config.NewServiceConfig("test", c)
|
||||
backend := listenbrainz.ListenBrainzApiBackend{}
|
||||
backend := lbapi.ListenBrainzApiBackend{}
|
||||
err := backend.InitConfig(&service)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
@ -57,7 +58,7 @@ func TestListenBrainzListenAsListen(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
listen := lbListen.AsListen()
|
||||
listen := lbapi.AsListen(lbListen)
|
||||
assert.Equal(t, time.Unix(1699289873, 0), listen.ListenedAt)
|
||||
assert.Equal(t, lbListen.UserName, listen.UserName)
|
||||
assert.Equal(t, time.Duration(413787*time.Millisecond), listen.Duration)
|
||||
|
@ -93,7 +94,7 @@ func TestListenBrainzFeedbackAsLove(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
love := feedback.AsLove()
|
||||
love := lbapi.AsLove(feedback)
|
||||
assert := assert.New(t)
|
||||
assert.Equal(time.Unix(1699859066, 0).Unix(), love.Created.Unix())
|
||||
assert.Equal(feedback.UserName, love.UserName)
|
||||
|
@ -114,7 +115,7 @@ func TestListenBrainzPartialFeedbackAsLove(t *testing.T) {
|
|||
RecordingMBID: recordingMBID,
|
||||
Score: 1,
|
||||
}
|
||||
love := feedback.AsLove()
|
||||
love := lbapi.AsLove(feedback)
|
||||
assert := assert.New(t)
|
||||
assert.Equal(time.Unix(1699859066, 0).Unix(), love.Created.Unix())
|
||||
assert.Equal(recordingMBID, love.RecordingMBID)
|
||||
|
|
|
@ -28,7 +28,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/go-resty/resty/v2"
|
||||
"go.uploadedlobster.com/scotty/internal/version"
|
||||
"go.uploadedlobster.com/scotty/pkg/ratelimit"
|
||||
)
|
||||
|
||||
|
@ -44,13 +43,13 @@ type Client struct {
|
|||
MaxResults int
|
||||
}
|
||||
|
||||
func NewClient(token string) Client {
|
||||
func NewClient(token string, userAgent string) Client {
|
||||
client := resty.New()
|
||||
client.SetBaseURL(listenBrainzBaseURL)
|
||||
client.SetAuthScheme("Token")
|
||||
client.SetAuthToken(token)
|
||||
client.SetHeader("Accept", "application/json")
|
||||
client.SetHeader("User-Agent", version.UserAgent())
|
||||
client.SetHeader("User-Agent", userAgent)
|
||||
|
||||
// Handle rate limiting (see https://listenbrainz.readthedocs.io/en/latest/users/api/index.html#rate-limiting)
|
||||
ratelimit.EnableHTTPHeaderRateLimit(client, "X-RateLimit-Reset-In")
|
|
@ -31,12 +31,12 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uploadedlobster.com/mbtypes"
|
||||
"go.uploadedlobster.com/scotty/internal/backends/listenbrainz"
|
||||
"go.uploadedlobster.com/scotty/pkg/listenbrainz"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
token := "foobar123"
|
||||
client := listenbrainz.NewClient(token)
|
||||
client := listenbrainz.NewClient(token, "test/1.0")
|
||||
assert.Equal(t, token, client.HTTPClient.Token)
|
||||
assert.Equal(t, listenbrainz.DefaultItemsPerGet, client.MaxResults)
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ func TestNewClient(t *testing.T) {
|
|||
func TestGetListens(t *testing.T) {
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
client := listenbrainz.NewClient("thetoken", "test/1.0")
|
||||
client.MaxResults = 2
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://api.listenbrainz.org/1/user/outsidecontext/listens",
|
||||
|
@ -64,7 +64,7 @@ func TestGetListens(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSubmitListens(t *testing.T) {
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
client := listenbrainz.NewClient("thetoken", "test/1.0")
|
||||
httpmock.ActivateNonDefault(client.HTTPClient.GetClient())
|
||||
|
||||
responder, err := httpmock.NewJsonResponder(200, listenbrainz.StatusResult{
|
||||
|
@ -104,7 +104,7 @@ func TestSubmitListens(t *testing.T) {
|
|||
func TestGetFeedback(t *testing.T) {
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
client := listenbrainz.NewClient("thetoken", "test/1.0")
|
||||
client.MaxResults = 2
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://api.listenbrainz.org/1/feedback/user/outsidecontext/get-feedback",
|
||||
|
@ -123,7 +123,7 @@ func TestGetFeedback(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSendFeedback(t *testing.T) {
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
client := listenbrainz.NewClient("thetoken", "test/1.0")
|
||||
httpmock.ActivateNonDefault(client.HTTPClient.GetClient())
|
||||
|
||||
responder, err := httpmock.NewJsonResponder(200, listenbrainz.StatusResult{
|
||||
|
@ -149,7 +149,7 @@ func TestSendFeedback(t *testing.T) {
|
|||
func TestLookup(t *testing.T) {
|
||||
defer httpmock.DeactivateAndReset()
|
||||
|
||||
client := listenbrainz.NewClient("thetoken")
|
||||
client := listenbrainz.NewClient("thetoken", "test/1.0")
|
||||
setupHTTPMock(t, client.HTTPClient.GetClient(),
|
||||
"https://api.listenbrainz.org/1/metadata/lookup",
|
||||
"testdata/lookup.json")
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -66,16 +66,19 @@ type Track struct {
|
|||
TrackName string `json:"track_name,omitempty"`
|
||||
ArtistName string `json:"artist_name,omitempty"`
|
||||
ReleaseName string `json:"release_name,omitempty"`
|
||||
RecordingMSID string `json:"recording_msid,omitempty"`
|
||||
AdditionalInfo map[string]any `json:"additional_info,omitempty"`
|
||||
MBIDMapping *MBIDMapping `json:"mbid_mapping,omitempty"`
|
||||
}
|
||||
|
||||
type MBIDMapping struct {
|
||||
RecordingName string `json:"recording_name,omitempty"`
|
||||
RecordingMBID mbtypes.MBID `json:"recording_mbid,omitempty"`
|
||||
ReleaseMBID mbtypes.MBID `json:"release_mbid,omitempty"`
|
||||
ArtistMBIDs []mbtypes.MBID `json:"artist_mbids,omitempty"`
|
||||
Artists []Artist `json:"artists,omitempty"`
|
||||
ArtistMBIDs []mbtypes.MBID `json:"artist_mbids,omitempty"`
|
||||
Artists []Artist `json:"artists,omitempty"`
|
||||
RecordingMBID mbtypes.MBID `json:"recording_mbid,omitempty"`
|
||||
RecordingName string `json:"recording_name,omitempty"`
|
||||
ReleaseMBID mbtypes.MBID `json:"release_mbid,omitempty"`
|
||||
CAAID int `json:"caa_id,omitempty"`
|
||||
CAAReleaseMBID mbtypes.MBID `json:"caa_release_mbid,omitempty"`
|
||||
}
|
||||
|
||||
type Artist struct {
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright © 2023 Philipp Wolfer <phw@uploadedlobster.com>
|
||||
Copyright © 2023-2025 Philipp Wolfer <phw@uploadedlobster.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -29,7 +29,7 @@ import (
|
|||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uploadedlobster.com/mbtypes"
|
||||
"go.uploadedlobster.com/scotty/internal/backends/listenbrainz"
|
||||
"go.uploadedlobster.com/scotty/pkg/listenbrainz"
|
||||
)
|
||||
|
||||
func TestTrackDurationMillisecondsInt(t *testing.T) {
|
Loading…
Add table
Add a link
Reference in a new issue