mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-04 13:07:36 +03:00
Send NowPlaying and Scrobbles to Last.fm
This commit is contained in:
parent
d5461d0ae9
commit
a7509c9ff7
15 changed files with 503 additions and 78 deletions
|
@ -46,7 +46,7 @@ const (
|
||||||
PlaceholderAlbumArt = "navidrome-600x600.png"
|
PlaceholderAlbumArt = "navidrome-600x600.png"
|
||||||
PlaceholderAvatar = "logo-192x192.png"
|
PlaceholderAvatar = "logo-192x192.png"
|
||||||
|
|
||||||
DefaultCachedHttpClientTTL = 10 * time.Second
|
DefaultHttpClientTimeOut = 10 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cache options
|
// Cache options
|
||||||
|
|
|
@ -4,6 +4,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
|
@ -12,15 +15,17 @@ import (
|
||||||
var _ = Describe("Agents", func() {
|
var _ = Describe("Agents", func() {
|
||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
|
var ds model.DataStore
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
ctx, cancel = context.WithCancel(context.Background())
|
ctx, cancel = context.WithCancel(context.Background())
|
||||||
|
ds = &tests.MockDataStore{}
|
||||||
})
|
})
|
||||||
|
|
||||||
Describe("Placeholder", func() {
|
Describe("Placeholder", func() {
|
||||||
var ag *Agents
|
var ag *Agents
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
conf.Server.Agents = ""
|
conf.Server.Agents = ""
|
||||||
ag = New(ctx)
|
ag = New(ds)
|
||||||
})
|
})
|
||||||
|
|
||||||
It("calls the placeholder GetBiography", func() {
|
It("calls the placeholder GetBiography", func() {
|
||||||
|
@ -41,16 +46,16 @@ var _ = Describe("Agents", func() {
|
||||||
var mock *mockAgent
|
var mock *mockAgent
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
mock = &mockAgent{}
|
mock = &mockAgent{}
|
||||||
Register("fake", func(ctx context.Context) Interface {
|
Register("fake", func(ds model.DataStore) Interface {
|
||||||
return mock
|
return mock
|
||||||
})
|
})
|
||||||
Register("empty", func(ctx context.Context) Interface {
|
Register("empty", func(ds model.DataStore) Interface {
|
||||||
return struct {
|
return struct {
|
||||||
Interface
|
Interface
|
||||||
}{}
|
}{}
|
||||||
})
|
})
|
||||||
conf.Server.Agents = "empty,fake"
|
conf.Server.Agents = "empty,fake"
|
||||||
ag = New(ctx)
|
ag = New(ds)
|
||||||
Expect(ag.AgentName()).To(Equal("agents"))
|
Expect(ag.AgentName()).To(Equal("agents"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -18,23 +18,27 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type lastfmAgent struct {
|
type lastfmAgent struct {
|
||||||
ctx context.Context
|
ds model.DataStore
|
||||||
ds model.DataStore
|
sessionKeys *sessionKeys
|
||||||
apiKey string
|
apiKey string
|
||||||
secret string
|
secret string
|
||||||
lang string
|
lang string
|
||||||
client *Client
|
client *Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func lastFMConstructor(ds model.DataStore) *lastfmAgent {
|
func lastFMConstructor(ds model.DataStore) *lastfmAgent {
|
||||||
l := &lastfmAgent{
|
l := &lastfmAgent{
|
||||||
ds: ds,
|
ds: ds,
|
||||||
lang: conf.Server.LastFM.Language,
|
lang: conf.Server.LastFM.Language,
|
||||||
apiKey: conf.Server.LastFM.ApiKey,
|
apiKey: conf.Server.LastFM.ApiKey,
|
||||||
secret: conf.Server.LastFM.Secret,
|
secret: conf.Server.LastFM.Secret,
|
||||||
|
sessionKeys: &sessionKeys{ds: ds},
|
||||||
}
|
}
|
||||||
hc := utils.NewCachedHTTPClient(http.DefaultClient, consts.DefaultCachedHttpClientTTL)
|
hc := &http.Client{
|
||||||
l.client = NewClient(l.apiKey, l.secret, l.lang, hc)
|
Timeout: consts.DefaultHttpClientTimeOut,
|
||||||
|
}
|
||||||
|
chc := utils.NewCachedHTTPClient(hc, consts.DefaultHttpClientTimeOut)
|
||||||
|
l.client = NewClient(l.apiKey, l.secret, l.lang, chc)
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +47,7 @@ func (l *lastfmAgent) AgentName() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lastfmAgent) GetMBID(ctx context.Context, id string, name string) (string, error) {
|
func (l *lastfmAgent) GetMBID(ctx context.Context, id string, name string) (string, error) {
|
||||||
a, err := l.callArtistGetInfo(name, "")
|
a, err := l.callArtistGetInfo(ctx, name, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -54,7 +58,7 @@ func (l *lastfmAgent) GetMBID(ctx context.Context, id string, name string) (stri
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lastfmAgent) GetURL(ctx context.Context, id, name, mbid string) (string, error) {
|
func (l *lastfmAgent) GetURL(ctx context.Context, id, name, mbid string) (string, error) {
|
||||||
a, err := l.callArtistGetInfo(name, mbid)
|
a, err := l.callArtistGetInfo(ctx, name, mbid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -65,7 +69,7 @@ func (l *lastfmAgent) GetURL(ctx context.Context, id, name, mbid string) (string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lastfmAgent) GetBiography(ctx context.Context, id, name, mbid string) (string, error) {
|
func (l *lastfmAgent) GetBiography(ctx context.Context, id, name, mbid string) (string, error) {
|
||||||
a, err := l.callArtistGetInfo(name, mbid)
|
a, err := l.callArtistGetInfo(ctx, name, mbid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -76,7 +80,7 @@ func (l *lastfmAgent) GetBiography(ctx context.Context, id, name, mbid string) (
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lastfmAgent) GetSimilar(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) {
|
func (l *lastfmAgent) GetSimilar(ctx context.Context, id, name, mbid string, limit int) ([]agents.Artist, error) {
|
||||||
resp, err := l.callArtistGetSimilar(name, mbid, limit)
|
resp, err := l.callArtistGetSimilar(ctx, name, mbid, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -94,7 +98,7 @@ func (l *lastfmAgent) GetSimilar(ctx context.Context, id, name, mbid string, lim
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lastfmAgent) GetTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) {
|
func (l *lastfmAgent) GetTopSongs(ctx context.Context, id, artistName, mbid string, count int) ([]agents.Song, error) {
|
||||||
resp, err := l.callArtistGetTopTracks(artistName, mbid, count)
|
resp, err := l.callArtistGetTopTracks(ctx, artistName, mbid, count)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -111,54 +115,91 @@ func (l *lastfmAgent) GetTopSongs(ctx context.Context, id, artistName, mbid stri
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lastfmAgent) callArtistGetInfo(name string, mbid string) (*Artist, error) {
|
func (l *lastfmAgent) callArtistGetInfo(ctx context.Context, name string, mbid string) (*Artist, error) {
|
||||||
a, err := l.client.ArtistGetInfo(l.ctx, name, mbid)
|
a, err := l.client.ArtistGetInfo(ctx, name, mbid)
|
||||||
lfErr, isLastFMError := err.(*lastFMError)
|
lfErr, isLastFMError := err.(*lastFMError)
|
||||||
if mbid != "" && ((err == nil && a.Name == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
|
if mbid != "" && ((err == nil && a.Name == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
|
||||||
log.Warn(l.ctx, "LastFM/artist.getInfo could not find artist by mbid, trying again", "artist", name, "mbid", mbid)
|
log.Warn(ctx, "LastFM/artist.getInfo could not find artist by mbid, trying again", "artist", name, "mbid", mbid)
|
||||||
return l.callArtistGetInfo(name, "")
|
return l.callArtistGetInfo(ctx, name, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(l.ctx, "Error calling LastFM/artist.getInfo", "artist", name, "mbid", mbid, err)
|
log.Error(ctx, "Error calling LastFM/artist.getInfo", "artist", name, "mbid", mbid, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lastfmAgent) callArtistGetSimilar(name string, mbid string, limit int) ([]Artist, error) {
|
func (l *lastfmAgent) callArtistGetSimilar(ctx context.Context, name string, mbid string, limit int) ([]Artist, error) {
|
||||||
s, err := l.client.ArtistGetSimilar(l.ctx, name, mbid, limit)
|
s, err := l.client.ArtistGetSimilar(ctx, name, mbid, limit)
|
||||||
lfErr, isLastFMError := err.(*lastFMError)
|
lfErr, isLastFMError := err.(*lastFMError)
|
||||||
if mbid != "" && ((err == nil && s.Attr.Artist == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
|
if mbid != "" && ((err == nil && s.Attr.Artist == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
|
||||||
log.Warn(l.ctx, "LastFM/artist.getSimilar could not find artist by mbid, trying again", "artist", name, "mbid", mbid)
|
log.Warn(ctx, "LastFM/artist.getSimilar could not find artist by mbid, trying again", "artist", name, "mbid", mbid)
|
||||||
return l.callArtistGetSimilar(name, "", limit)
|
return l.callArtistGetSimilar(ctx, name, "", limit)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(l.ctx, "Error calling LastFM/artist.getSimilar", "artist", name, "mbid", mbid, err)
|
log.Error(ctx, "Error calling LastFM/artist.getSimilar", "artist", name, "mbid", mbid, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return s.Artists, nil
|
return s.Artists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lastfmAgent) callArtistGetTopTracks(artistName, mbid string, count int) ([]Track, error) {
|
func (l *lastfmAgent) callArtistGetTopTracks(ctx context.Context, artistName, mbid string, count int) ([]Track, error) {
|
||||||
t, err := l.client.ArtistGetTopTracks(l.ctx, artistName, mbid, count)
|
t, err := l.client.ArtistGetTopTracks(ctx, artistName, mbid, count)
|
||||||
lfErr, isLastFMError := err.(*lastFMError)
|
lfErr, isLastFMError := err.(*lastFMError)
|
||||||
if mbid != "" && ((err == nil && t.Attr.Artist == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
|
if mbid != "" && ((err == nil && t.Attr.Artist == "[unknown]") || (isLastFMError && lfErr.Code == 6)) {
|
||||||
log.Warn(l.ctx, "LastFM/artist.getTopTracks could not find artist by mbid, trying again", "artist", artistName, "mbid", mbid)
|
log.Warn(ctx, "LastFM/artist.getTopTracks could not find artist by mbid, trying again", "artist", artistName, "mbid", mbid)
|
||||||
return l.callArtistGetTopTracks(artistName, "", count)
|
return l.callArtistGetTopTracks(ctx, artistName, "", count)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(l.ctx, "Error calling LastFM/artist.getTopTracks", "artist", artistName, "mbid", mbid, err)
|
log.Error(ctx, "Error calling LastFM/artist.getTopTracks", "artist", artistName, "mbid", mbid, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return t.Track, nil
|
return t.Track, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lastfmAgent) NowPlaying(c context.Context, track *model.MediaFile) error {
|
func (l *lastfmAgent) NowPlaying(ctx context.Context, userId string, track *model.MediaFile) error {
|
||||||
|
sk, err := l.sessionKeys.get(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = l.client.UpdateNowPlaying(ctx, sk, ScrobbleInfo{
|
||||||
|
artist: track.Artist,
|
||||||
|
track: track.Title,
|
||||||
|
album: track.Album,
|
||||||
|
trackNumber: track.TrackNumber,
|
||||||
|
mbid: track.MbzTrackID,
|
||||||
|
duration: int(track.Duration),
|
||||||
|
albumArtist: track.AlbumArtist,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *lastfmAgent) Scrobble(ctx context.Context, scrobbles []scrobbler.Scrobble) error {
|
func (l *lastfmAgent) Scrobble(ctx context.Context, userId string, scrobbles []scrobbler.Scrobble) error {
|
||||||
|
sk, err := l.sessionKeys.get(ctx, userId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Implement batch scrobbling
|
||||||
|
for _, s := range scrobbles {
|
||||||
|
err = l.client.Scrobble(ctx, sk, ScrobbleInfo{
|
||||||
|
artist: s.Artist,
|
||||||
|
track: s.Title,
|
||||||
|
album: s.Album,
|
||||||
|
trackNumber: s.TrackNumber,
|
||||||
|
mbid: s.MbzTrackID,
|
||||||
|
duration: int(s.Duration),
|
||||||
|
albumArtist: s.AlbumArtist,
|
||||||
|
timestamp: s.TimeStamp,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,14 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/core/scrobbler"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/model/request"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/conf"
|
"github.com/navidrome/navidrome/conf"
|
||||||
"github.com/navidrome/navidrome/core/agents"
|
"github.com/navidrome/navidrome/core/agents"
|
||||||
|
@ -21,15 +29,21 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("lastfmAgent", func() {
|
var _ = Describe("lastfmAgent", func() {
|
||||||
|
var ds model.DataStore
|
||||||
|
var ctx context.Context
|
||||||
|
BeforeEach(func() {
|
||||||
|
ds = &tests.MockDataStore{}
|
||||||
|
ctx = context.Background()
|
||||||
|
})
|
||||||
Describe("lastFMConstructor", func() {
|
Describe("lastFMConstructor", func() {
|
||||||
It("uses configured api key and language", func() {
|
It("uses configured api key and language", func() {
|
||||||
conf.Server.LastFM.ApiKey = "123"
|
conf.Server.LastFM.ApiKey = "123"
|
||||||
conf.Server.LastFM.Secret = "secret"
|
conf.Server.LastFM.Secret = "secret"
|
||||||
conf.Server.LastFM.Language = "pt"
|
conf.Server.LastFM.Language = "pt"
|
||||||
agent := lastFMConstructor(context.Background())
|
agent := lastFMConstructor(ds)
|
||||||
Expect(agent.(*lastfmAgent).apiKey).To(Equal("123"))
|
Expect(agent.apiKey).To(Equal("123"))
|
||||||
Expect(agent.(*lastfmAgent).secret).To(Equal("secret"))
|
Expect(agent.secret).To(Equal("secret"))
|
||||||
Expect(agent.(*lastfmAgent).lang).To(Equal("pt"))
|
Expect(agent.lang).To(Equal("pt"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -39,7 +53,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
httpClient = &tests.FakeHttpClient{}
|
httpClient = &tests.FakeHttpClient{}
|
||||||
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
|
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
|
||||||
agent = lastFMConstructor(context.Background()).(*lastfmAgent)
|
agent = lastFMConstructor(ds)
|
||||||
agent.client = client
|
agent.client = client
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -97,7 +111,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
httpClient = &tests.FakeHttpClient{}
|
httpClient = &tests.FakeHttpClient{}
|
||||||
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
|
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
|
||||||
agent = lastFMConstructor(context.Background()).(*lastfmAgent)
|
agent = lastFMConstructor(ds)
|
||||||
agent.client = client
|
agent.client = client
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -158,7 +172,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
httpClient = &tests.FakeHttpClient{}
|
httpClient = &tests.FakeHttpClient{}
|
||||||
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
|
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
|
||||||
agent = lastFMConstructor(context.Background()).(*lastfmAgent)
|
agent = lastFMConstructor(ds)
|
||||||
agent.client = client
|
agent.client = client
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -212,4 +226,74 @@ var _ = Describe("lastfmAgent", func() {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("Scrobbling", func() {
|
||||||
|
var agent *lastfmAgent
|
||||||
|
var httpClient *tests.FakeHttpClient
|
||||||
|
var track *model.MediaFile
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx = request.WithUser(ctx, model.User{ID: "user-1"})
|
||||||
|
_ = ds.Property(ctx).Put(sessionKeyPropertyPrefix+"user-1", "SK-1")
|
||||||
|
httpClient = &tests.FakeHttpClient{}
|
||||||
|
client := NewClient("API_KEY", "SECRET", "en", httpClient)
|
||||||
|
agent = lastFMConstructor(ds)
|
||||||
|
agent.client = client
|
||||||
|
track = &model.MediaFile{
|
||||||
|
ID: "123",
|
||||||
|
Title: "Track Title",
|
||||||
|
Album: "Track Album",
|
||||||
|
Artist: "Track Artist",
|
||||||
|
AlbumArtist: "Track AlbumArtist",
|
||||||
|
TrackNumber: 1,
|
||||||
|
Duration: 180,
|
||||||
|
MbzTrackID: "mbz-123",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("NowPlaying", func() {
|
||||||
|
It("calls Last.fm with correct params", func() {
|
||||||
|
httpClient.Res = http.Response{Body: ioutil.NopCloser(bytes.NewBufferString("{}")), StatusCode: 200}
|
||||||
|
|
||||||
|
err := agent.NowPlaying(ctx, "user-1", track)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(httpClient.SavedRequest.Method).To(Equal(http.MethodPost))
|
||||||
|
sentParams := httpClient.SavedRequest.URL.Query()
|
||||||
|
Expect(sentParams.Get("method")).To(Equal("track.updateNowPlaying"))
|
||||||
|
Expect(sentParams.Get("sk")).To(Equal("SK-1"))
|
||||||
|
Expect(sentParams.Get("track")).To(Equal(track.Title))
|
||||||
|
Expect(sentParams.Get("album")).To(Equal(track.Album))
|
||||||
|
Expect(sentParams.Get("artist")).To(Equal(track.Artist))
|
||||||
|
Expect(sentParams.Get("albumArtist")).To(Equal(track.AlbumArtist))
|
||||||
|
Expect(sentParams.Get("trackNumber")).To(Equal(strconv.Itoa(track.TrackNumber)))
|
||||||
|
Expect(sentParams.Get("duration")).To(Equal(strconv.FormatFloat(float64(track.Duration), 'G', -1, 32)))
|
||||||
|
Expect(sentParams.Get("mbid")).To(Equal(track.MbzTrackID))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("Scrobble", func() {
|
||||||
|
It("calls Last.fm with correct params", func() {
|
||||||
|
ts := time.Now()
|
||||||
|
scrobbles := []scrobbler.Scrobble{{MediaFile: *track, TimeStamp: ts}}
|
||||||
|
httpClient.Res = http.Response{Body: ioutil.NopCloser(bytes.NewBufferString("{}")), StatusCode: 200}
|
||||||
|
|
||||||
|
err := agent.Scrobble(ctx, "user-1", scrobbles)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(httpClient.SavedRequest.Method).To(Equal(http.MethodPost))
|
||||||
|
sentParams := httpClient.SavedRequest.URL.Query()
|
||||||
|
Expect(sentParams.Get("method")).To(Equal("track.scrobble"))
|
||||||
|
Expect(sentParams.Get("sk")).To(Equal("SK-1"))
|
||||||
|
Expect(sentParams.Get("track")).To(Equal(track.Title))
|
||||||
|
Expect(sentParams.Get("album")).To(Equal(track.Album))
|
||||||
|
Expect(sentParams.Get("artist")).To(Equal(track.Artist))
|
||||||
|
Expect(sentParams.Get("albumArtist")).To(Equal(track.AlbumArtist))
|
||||||
|
Expect(sentParams.Get("trackNumber")).To(Equal(strconv.Itoa(track.TrackNumber)))
|
||||||
|
Expect(sentParams.Get("duration")).To(Equal(strconv.FormatFloat(float64(track.Duration), 'G', -1, 32)))
|
||||||
|
Expect(sentParams.Get("mbid")).To(Equal(track.MbzTrackID))
|
||||||
|
Expect(sentParams.Get("timestamp")).To(Equal(strconv.FormatInt(ts.Unix(), 10)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/consts"
|
||||||
|
|
||||||
"github.com/deluan/rest"
|
"github.com/deluan/rest"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/chi/v5/middleware"
|
"github.com/go-chi/chi/v5/middleware"
|
||||||
|
@ -32,13 +34,16 @@ type Router struct {
|
||||||
|
|
||||||
func NewRouter(ds model.DataStore) *Router {
|
func NewRouter(ds model.DataStore) *Router {
|
||||||
r := &Router{
|
r := &Router{
|
||||||
ds: ds,
|
ds: ds,
|
||||||
apiKey: conf.Server.LastFM.ApiKey,
|
apiKey: conf.Server.LastFM.ApiKey,
|
||||||
secret: conf.Server.LastFM.Secret,
|
secret: conf.Server.LastFM.Secret,
|
||||||
|
sessionKeys: &sessionKeys{ds: ds},
|
||||||
}
|
}
|
||||||
r.sessionKeys = &sessionKeys{ds: ds}
|
|
||||||
r.Handler = r.routes()
|
r.Handler = r.routes()
|
||||||
r.client = NewClient(r.apiKey, r.secret, "en", http.DefaultClient)
|
hc := &http.Client{
|
||||||
|
Timeout: consts.DefaultHttpClientTimeOut,
|
||||||
|
}
|
||||||
|
r.client = NewClient(r.apiKey, r.secret, "en", hc)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,15 @@ import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
"github.com/navidrome/navidrome/utils"
|
"github.com/navidrome/navidrome/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -49,7 +52,7 @@ func (c *Client) ArtistGetInfo(ctx context.Context, name string, mbid string) (*
|
||||||
params.Add("artist", name)
|
params.Add("artist", name)
|
||||||
params.Add("mbid", mbid)
|
params.Add("mbid", mbid)
|
||||||
params.Add("lang", c.lang)
|
params.Add("lang", c.lang)
|
||||||
response, err := c.makeRequest(params, false)
|
response, err := c.makeRequest(http.MethodGet, params, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -62,7 +65,7 @@ func (c *Client) ArtistGetSimilar(ctx context.Context, name string, mbid string,
|
||||||
params.Add("artist", name)
|
params.Add("artist", name)
|
||||||
params.Add("mbid", mbid)
|
params.Add("mbid", mbid)
|
||||||
params.Add("limit", strconv.Itoa(limit))
|
params.Add("limit", strconv.Itoa(limit))
|
||||||
response, err := c.makeRequest(params, false)
|
response, err := c.makeRequest(http.MethodGet, params, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -75,7 +78,7 @@ func (c *Client) ArtistGetTopTracks(ctx context.Context, name string, mbid strin
|
||||||
params.Add("artist", name)
|
params.Add("artist", name)
|
||||||
params.Add("mbid", mbid)
|
params.Add("mbid", mbid)
|
||||||
params.Add("limit", strconv.Itoa(limit))
|
params.Add("limit", strconv.Itoa(limit))
|
||||||
response, err := c.makeRequest(params, false)
|
response, err := c.makeRequest(http.MethodGet, params, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -86,7 +89,7 @@ func (c *Client) GetToken(ctx context.Context) (string, error) {
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add("method", "auth.getToken")
|
params.Add("method", "auth.getToken")
|
||||||
c.sign(params)
|
c.sign(params)
|
||||||
response, err := c.makeRequest(params, true)
|
response, err := c.makeRequest(http.MethodGet, params, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -97,14 +100,74 @@ func (c *Client) GetSession(ctx context.Context, token string) (string, error) {
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add("method", "auth.getSession")
|
params.Add("method", "auth.getSession")
|
||||||
params.Add("token", token)
|
params.Add("token", token)
|
||||||
response, err := c.makeRequest(params, true)
|
response, err := c.makeRequest(http.MethodGet, params, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return response.Session.Key, nil
|
return response.Session.Key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) makeRequest(params url.Values, signed bool) (*Response, error) {
|
type ScrobbleInfo struct {
|
||||||
|
artist string
|
||||||
|
track string
|
||||||
|
album string
|
||||||
|
trackNumber int
|
||||||
|
mbid string
|
||||||
|
duration int
|
||||||
|
albumArtist string
|
||||||
|
timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) UpdateNowPlaying(ctx context.Context, sessionKey string, info ScrobbleInfo) error {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("method", "track.updateNowPlaying")
|
||||||
|
params.Add("artist", info.artist)
|
||||||
|
params.Add("track", info.track)
|
||||||
|
params.Add("album", info.album)
|
||||||
|
params.Add("trackNumber", strconv.Itoa(info.trackNumber))
|
||||||
|
params.Add("mbid", info.mbid)
|
||||||
|
params.Add("duration", strconv.Itoa(info.duration))
|
||||||
|
params.Add("albumArtist", info.albumArtist)
|
||||||
|
params.Add("sk", sessionKey)
|
||||||
|
resp, err := c.makeRequest(http.MethodPost, params, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.NowPlaying.IgnoredMessage.Code != "" {
|
||||||
|
log.Warn(ctx, "LastFM: NowPlaying was ignored", "code", resp.NowPlaying.IgnoredMessage.Code,
|
||||||
|
"text", resp.NowPlaying.IgnoredMessage.Text)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Scrobble(ctx context.Context, sessionKey string, info ScrobbleInfo) error {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("method", "track.scrobble")
|
||||||
|
params.Add("timestamp", strconv.FormatInt(info.timestamp.Unix(), 10))
|
||||||
|
params.Add("artist", info.artist)
|
||||||
|
params.Add("track", info.track)
|
||||||
|
params.Add("album", info.album)
|
||||||
|
params.Add("trackNumber", strconv.Itoa(info.trackNumber))
|
||||||
|
params.Add("mbid", info.mbid)
|
||||||
|
params.Add("duration", strconv.Itoa(info.duration))
|
||||||
|
params.Add("albumArtist", info.albumArtist)
|
||||||
|
params.Add("sk", sessionKey)
|
||||||
|
resp, err := c.makeRequest(http.MethodPost, params, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.Scrobbles.Scrobble.IgnoredMessage.Code != "0" {
|
||||||
|
log.Warn(ctx, "LastFM: Scrobble was ignored", "code", resp.Scrobbles.Scrobble.IgnoredMessage.Code,
|
||||||
|
"text", resp.Scrobbles.Scrobble.IgnoredMessage.Text)
|
||||||
|
}
|
||||||
|
if resp.Scrobbles.Attr.Accepted != 1 {
|
||||||
|
log.Warn(ctx, "LastFM: Scrobble was not accepted", "code", resp.Scrobbles.Scrobble.IgnoredMessage.Code,
|
||||||
|
"text", resp.Scrobbles.Scrobble.IgnoredMessage.Text)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) makeRequest(method string, params url.Values, signed bool) (*Response, error) {
|
||||||
params.Add("format", "json")
|
params.Add("format", "json")
|
||||||
params.Add("api_key", c.apiKey)
|
params.Add("api_key", c.apiKey)
|
||||||
|
|
||||||
|
@ -112,7 +175,7 @@ func (c *Client) makeRequest(params url.Values, signed bool) (*Response, error)
|
||||||
c.sign(params)
|
c.sign(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", apiBaseUrl, nil)
|
req, _ := http.NewRequest(method, apiBaseUrl, nil)
|
||||||
req.URL.RawQuery = params.Encode()
|
req.URL.RawQuery = params.Encode()
|
||||||
|
|
||||||
resp, err := c.hc.Do(req)
|
resp, err := c.hc.Do(req)
|
||||||
|
@ -121,7 +184,8 @@ func (c *Client) makeRequest(params url.Values, signed bool) (*Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
decoder := json.NewDecoder(resp.Body)
|
body, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
decoder := json.NewDecoder(strings.NewReader(string(body)))
|
||||||
|
|
||||||
var response Response
|
var response Response
|
||||||
jsonErr := decoder.Decode(&response)
|
jsonErr := decoder.Decode(&response)
|
||||||
|
|
|
@ -8,6 +8,8 @@ type Response struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Session Session `json:"session"`
|
Session Session `json:"session"`
|
||||||
|
NowPlaying NowPlaying `json:"nowplaying"`
|
||||||
|
Scrobbles Scrobbles `json:"scrobbles"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Artist struct {
|
type Artist struct {
|
||||||
|
@ -67,3 +69,56 @@ type Session struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Subscriber int `json:"subscriber"`
|
Subscriber int `json:"subscriber"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NowPlaying struct {
|
||||||
|
Artist struct {
|
||||||
|
Corrected string `json:"corrected"`
|
||||||
|
Text string `json:"#text"`
|
||||||
|
} `json:"artist"`
|
||||||
|
IgnoredMessage struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Text string `json:"#text"`
|
||||||
|
} `json:"ignoredMessage"`
|
||||||
|
Album struct {
|
||||||
|
Corrected string `json:"corrected"`
|
||||||
|
Text string `json:"#text"`
|
||||||
|
} `json:"album"`
|
||||||
|
AlbumArtist struct {
|
||||||
|
Corrected string `json:"corrected"`
|
||||||
|
Text string `json:"#text"`
|
||||||
|
} `json:"albumArtist"`
|
||||||
|
Track struct {
|
||||||
|
Corrected string `json:"corrected"`
|
||||||
|
Text string `json:"#text"`
|
||||||
|
} `json:"track"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Scrobbles struct {
|
||||||
|
Attr struct {
|
||||||
|
Accepted int `json:"accepted"`
|
||||||
|
Ignored int `json:"ignored"`
|
||||||
|
} `json:"@attr"`
|
||||||
|
Scrobble struct {
|
||||||
|
Artist struct {
|
||||||
|
Corrected string `json:"corrected"`
|
||||||
|
Text string `json:"#text"`
|
||||||
|
} `json:"artist"`
|
||||||
|
IgnoredMessage struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Text string `json:"#text"`
|
||||||
|
} `json:"ignoredMessage"`
|
||||||
|
AlbumArtist struct {
|
||||||
|
Corrected string `json:"corrected"`
|
||||||
|
Text string `json:"#text"`
|
||||||
|
} `json:"albumArtist"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
Album struct {
|
||||||
|
Corrected string `json:"corrected"`
|
||||||
|
Text string `json:"#text"`
|
||||||
|
} `json:"album"`
|
||||||
|
Track struct {
|
||||||
|
Corrected string `json:"corrected"`
|
||||||
|
Text string `json:"#text"`
|
||||||
|
} `json:"track"`
|
||||||
|
} `json:"scrobble"`
|
||||||
|
}
|
||||||
|
|
|
@ -31,8 +31,11 @@ func spotifyConstructor(ds model.DataStore) agents.Interface {
|
||||||
id: conf.Server.Spotify.ID,
|
id: conf.Server.Spotify.ID,
|
||||||
secret: conf.Server.Spotify.Secret,
|
secret: conf.Server.Spotify.Secret,
|
||||||
}
|
}
|
||||||
hc := utils.NewCachedHTTPClient(http.DefaultClient, consts.DefaultCachedHttpClientTTL)
|
hc := &http.Client{
|
||||||
l.client = NewClient(l.id, l.secret, hc)
|
Timeout: consts.DefaultHttpClientTimeOut,
|
||||||
|
}
|
||||||
|
chc := utils.NewCachedHTTPClient(hc, consts.DefaultHttpClientTimeOut)
|
||||||
|
l.client = NewClient(l.id, l.secret, chc)
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ type NowPlayingInfo struct {
|
||||||
type Broker interface {
|
type Broker interface {
|
||||||
NowPlaying(ctx context.Context, playerId string, playerName string, trackId string) error
|
NowPlaying(ctx context.Context, playerId string, playerName string, trackId string) error
|
||||||
GetNowPlaying(ctx context.Context) ([]NowPlayingInfo, error)
|
GetNowPlaying(ctx context.Context) ([]NowPlayingInfo, error)
|
||||||
Submit(ctx context.Context, playerId int, trackId string, playTime time.Time) error
|
Submit(ctx context.Context, trackId string, playTime time.Time) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type broker struct {
|
type broker struct {
|
||||||
|
@ -45,20 +45,20 @@ func GetBroker(ds model.DataStore) Broker {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *broker) NowPlaying(ctx context.Context, playerId string, playerName string, trackId string) error {
|
func (s *broker) NowPlaying(ctx context.Context, playerId string, playerName string, trackId string) error {
|
||||||
username, _ := request.UsernameFrom(ctx)
|
user, _ := request.UserFrom(ctx)
|
||||||
info := NowPlayingInfo{
|
info := NowPlayingInfo{
|
||||||
TrackID: trackId,
|
TrackID: trackId,
|
||||||
Start: time.Now(),
|
Start: time.Now(),
|
||||||
Username: username,
|
Username: user.UserName,
|
||||||
PlayerId: playerId,
|
PlayerId: playerId,
|
||||||
PlayerName: playerName,
|
PlayerName: playerName,
|
||||||
}
|
}
|
||||||
_ = s.playMap.Set(playerId, info)
|
_ = s.playMap.Set(playerId, info)
|
||||||
s.dispatchNowPlaying(ctx, trackId)
|
s.dispatchNowPlaying(ctx, user.ID, trackId)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *broker) dispatchNowPlaying(ctx context.Context, trackId string) {
|
func (s *broker) dispatchNowPlaying(ctx context.Context, userId string, trackId string) {
|
||||||
t, err := s.ds.MediaFile(ctx).Get(trackId)
|
t, err := s.ds.MediaFile(ctx).Get(trackId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Error retrieving mediaFile", "id", trackId, err)
|
log.Error(ctx, "Error retrieving mediaFile", "id", trackId, err)
|
||||||
|
@ -68,10 +68,8 @@ func (s *broker) dispatchNowPlaying(ctx context.Context, trackId string) {
|
||||||
for name, constructor := range scrobblers {
|
for name, constructor := range scrobblers {
|
||||||
log.Debug(ctx, "Sending NowPlaying info", "scrobbler", name, "track", t.Title, "artist", t.Artist)
|
log.Debug(ctx, "Sending NowPlaying info", "scrobbler", name, "track", t.Title, "artist", t.Artist)
|
||||||
err := func() error {
|
err := func() error {
|
||||||
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
s := constructor(s.ds)
|
s := constructor(s.ds)
|
||||||
return s.NowPlaying(ctx, t)
|
return s.NowPlaying(ctx, userId, t)
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(ctx, "Error sending NowPlayingInfo", "scrobbler", name, "track", t.Title, "artist", t.Artist, err)
|
log.Error(ctx, "Error sending NowPlayingInfo", "scrobbler", name, "track", t.Title, "artist", t.Artist, err)
|
||||||
|
@ -96,8 +94,27 @@ func (s *broker) GetNowPlaying(ctx context.Context) ([]NowPlayingInfo, error) {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *broker) Submit(ctx context.Context, playerId int, trackId string, playTime time.Time) error {
|
func (s *broker) Submit(ctx context.Context, trackId string, playTime time.Time) error {
|
||||||
panic("implement me")
|
u, _ := request.UserFrom(ctx)
|
||||||
|
t, err := s.ds.MediaFile(ctx).Get(trackId)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx, "Error retrieving mediaFile", "id", trackId, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
scrobbles := []Scrobble{{MediaFile: *t, TimeStamp: playTime}}
|
||||||
|
// TODO Parallelize
|
||||||
|
for name, constructor := range scrobblers {
|
||||||
|
log.Debug(ctx, "Sending NowPlaying info", "scrobbler", name, "track", t.Title, "artist", t.Artist)
|
||||||
|
err := func() error {
|
||||||
|
s := constructor(s.ds)
|
||||||
|
return s.Scrobble(ctx, u.ID, scrobbles)
|
||||||
|
}()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(ctx, "Error sending NowPlayingInfo", "scrobbler", name, "track", t.Title, "artist", t.Artist, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var scrobblers map[string]Constructor
|
var scrobblers map[string]Constructor
|
||||||
|
|
119
core/scrobbler/broker_test.go
Normal file
119
core/scrobbler/broker_test.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package scrobbler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/model"
|
||||||
|
"github.com/navidrome/navidrome/model/request"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Broker", func() {
|
||||||
|
var ctx context.Context
|
||||||
|
var ds model.DataStore
|
||||||
|
var broker Broker
|
||||||
|
var track model.MediaFile
|
||||||
|
var fake *fakeScrobbler
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx = context.Background()
|
||||||
|
ctx = request.WithUser(ctx, model.User{ID: "u-1"})
|
||||||
|
ds = &tests.MockDataStore{}
|
||||||
|
broker = GetBroker(ds)
|
||||||
|
fake = &fakeScrobbler{}
|
||||||
|
Register("fake", func(ds model.DataStore) Scrobbler {
|
||||||
|
return fake
|
||||||
|
})
|
||||||
|
|
||||||
|
track = model.MediaFile{
|
||||||
|
ID: "123",
|
||||||
|
Title: "Track Title",
|
||||||
|
Album: "Track Album",
|
||||||
|
Artist: "Track Artist",
|
||||||
|
AlbumArtist: "Track AlbumArtist",
|
||||||
|
TrackNumber: 1,
|
||||||
|
Duration: 180,
|
||||||
|
MbzTrackID: "mbz-123",
|
||||||
|
}
|
||||||
|
_ = ds.MediaFile(ctx).Put(&track)
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("NowPlaying", func() {
|
||||||
|
It("sends track to agent", func() {
|
||||||
|
err := broker.NowPlaying(ctx, "player-1", "player-one", "123")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(fake.UserID).To(Equal("u-1"))
|
||||||
|
Expect(fake.Track.ID).To(Equal("123"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("GetNowPlaying", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx = context.Background()
|
||||||
|
})
|
||||||
|
It("returns current playing music", func() {
|
||||||
|
track2 := track
|
||||||
|
track2.ID = "456"
|
||||||
|
_ = ds.MediaFile(ctx).Put(&track)
|
||||||
|
ctx = request.WithUser(ctx, model.User{UserName: "user-1"})
|
||||||
|
_ = broker.NowPlaying(ctx, "player-1", "player-one", "123")
|
||||||
|
ctx = request.WithUser(ctx, model.User{UserName: "user-2"})
|
||||||
|
_ = broker.NowPlaying(ctx, "player-2", "player-two", "456")
|
||||||
|
|
||||||
|
playing, err := broker.GetNowPlaying(ctx)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(playing).To(HaveLen(2))
|
||||||
|
Expect(playing[0].PlayerId).To(Equal("player-2"))
|
||||||
|
Expect(playing[0].PlayerName).To(Equal("player-two"))
|
||||||
|
Expect(playing[0].Username).To(Equal("user-2"))
|
||||||
|
Expect(playing[0].TrackID).To(Equal("456"))
|
||||||
|
|
||||||
|
Expect(playing[1].PlayerId).To(Equal("player-1"))
|
||||||
|
Expect(playing[1].PlayerName).To(Equal("player-one"))
|
||||||
|
Expect(playing[1].Username).To(Equal("user-1"))
|
||||||
|
Expect(playing[1].TrackID).To(Equal("123"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("Submit", func() {
|
||||||
|
It("sends track to agent", func() {
|
||||||
|
ctx = request.WithUser(ctx, model.User{ID: "u-1", UserName: "user-1"})
|
||||||
|
ts := time.Now()
|
||||||
|
|
||||||
|
err := broker.Submit(ctx, "123", ts)
|
||||||
|
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
Expect(fake.UserID).To(Equal("u-1"))
|
||||||
|
Expect(fake.Scrobbles[0].ID).To(Equal("123"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
type fakeScrobbler struct {
|
||||||
|
UserID string
|
||||||
|
Track *model.MediaFile
|
||||||
|
Scrobbles []Scrobble
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeScrobbler) NowPlaying(ctx context.Context, userId string, track *model.MediaFile) error {
|
||||||
|
if f.Error != nil {
|
||||||
|
return f.Error
|
||||||
|
}
|
||||||
|
f.UserID = userId
|
||||||
|
f.Track = track
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeScrobbler) Scrobble(ctx context.Context, userId string, scrobbles []Scrobble) error {
|
||||||
|
if f.Error != nil {
|
||||||
|
return f.Error
|
||||||
|
}
|
||||||
|
f.UserID = userId
|
||||||
|
f.Scrobbles = scrobbles
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -8,13 +8,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Scrobble struct {
|
type Scrobble struct {
|
||||||
Track *model.MediaFile
|
model.MediaFile
|
||||||
TimeStamp *time.Time
|
TimeStamp time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type Scrobbler interface {
|
type Scrobbler interface {
|
||||||
NowPlaying(context.Context, *model.MediaFile) error
|
NowPlaying(ctx context.Context, userId string, track *model.MediaFile) error
|
||||||
Scrobble(context.Context, []Scrobble) error
|
Scrobble(ctx context.Context, userId string, scrobbles []Scrobble) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Constructor func(ds model.DataStore) Scrobbler
|
type Constructor func(ds model.DataStore) Scrobbler
|
||||||
|
|
17
core/scrobbler/scrobbler_suite_test.go
Normal file
17
core/scrobbler/scrobbler_suite_test.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package scrobbler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/log"
|
||||||
|
"github.com/navidrome/navidrome/tests"
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAgents(t *testing.T) {
|
||||||
|
tests.Init(t, false)
|
||||||
|
log.SetLevel(log.LevelCritical)
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Scrobbler Test Suite")
|
||||||
|
}
|
|
@ -186,7 +186,7 @@ func (c *MediaAnnotationController) scrobblerRegister(ctx context.Context, track
|
||||||
} else {
|
} else {
|
||||||
log.Info("Scrobbled", "title", mf.Title, "artist", mf.Artist, "user", username)
|
log.Info("Scrobbled", "title", mf.Title, "artist", mf.Artist, "user", username)
|
||||||
}
|
}
|
||||||
|
_ = c.scrobbler.Submit(ctx, trackId, playTime)
|
||||||
return mf, err
|
return mf, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,15 @@ package tests
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/model"
|
"github.com/navidrome/navidrome/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateMockMediaFileRepo() *MockMediaFileRepo {
|
func CreateMockMediaFileRepo() *MockMediaFileRepo {
|
||||||
return &MockMediaFileRepo{}
|
return &MockMediaFileRepo{
|
||||||
|
data: make(map[string]model.MediaFile),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MockMediaFileRepo struct {
|
type MockMediaFileRepo struct {
|
||||||
|
@ -45,6 +49,17 @@ func (m *MockMediaFileRepo) Get(id string) (*model.MediaFile, error) {
|
||||||
return nil, model.ErrNotFound
|
return nil, model.ErrNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockMediaFileRepo) Put(mf *model.MediaFile) error {
|
||||||
|
if m.err {
|
||||||
|
return errors.New("error!")
|
||||||
|
}
|
||||||
|
if mf.ID == "" {
|
||||||
|
mf.ID = uuid.NewString()
|
||||||
|
}
|
||||||
|
m.data[mf.ID] = *mf
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockMediaFileRepo) FindByAlbum(artistId string) (model.MediaFiles, error) {
|
func (m *MockMediaFileRepo) FindByAlbum(artistId string) (model.MediaFiles, error) {
|
||||||
if m.err {
|
if m.err {
|
||||||
return nil, errors.New("Error!")
|
return nil, errors.New("Error!")
|
||||||
|
|
|
@ -25,7 +25,7 @@ var _ = Describe("CachedHttpClient", func() {
|
||||||
header = r.Header.Get("head")
|
header = r.Header.Get("head")
|
||||||
_, _ = fmt.Fprintf(w, "Hello, %s", r.URL.Query()["name"])
|
_, _ = fmt.Fprintf(w, "Hello, %s", r.URL.Query()["name"])
|
||||||
}))
|
}))
|
||||||
chc = NewCachedHTTPClient(http.DefaultClient, consts.DefaultCachedHttpClientTTL)
|
chc = NewCachedHTTPClient(http.DefaultClient, consts.DefaultHttpClientTimeOut)
|
||||||
})
|
})
|
||||||
|
|
||||||
AfterEach(func() {
|
AfterEach(func() {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue