mirror of
https://github.com/navidrome/navidrome.git
synced 2025-06-13 13:52:31 +03:00
Add Last.FM Authentication methods
This commit is contained in:
parent
73e1a8fa06
commit
a4f91b74d2
@ -2,12 +2,17 @@ package lastfm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/navidrome/navidrome/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -27,52 +32,17 @@ type httpDoer interface {
|
|||||||
Do(req *http.Request) (*http.Response, error)
|
Do(req *http.Request) (*http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(apiKey string, lang string, hc httpDoer) *Client {
|
func NewClient(apiKey string, secret string, lang string, hc httpDoer) *Client {
|
||||||
return &Client{apiKey, lang, hc}
|
return &Client{apiKey, secret, lang, hc}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
apiKey string
|
apiKey string
|
||||||
|
secret string
|
||||||
lang string
|
lang string
|
||||||
hc httpDoer
|
hc httpDoer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) makeRequest(params url.Values) (*Response, error) {
|
|
||||||
params.Add("format", "json")
|
|
||||||
params.Add("api_key", c.apiKey)
|
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", apiBaseUrl, nil)
|
|
||||||
req.URL.RawQuery = params.Encode()
|
|
||||||
|
|
||||||
resp, err := c.hc.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
data, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var response Response
|
|
||||||
jsonErr := json.Unmarshal(data, &response)
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 && jsonErr != nil {
|
|
||||||
return nil, fmt.Errorf("last.fm http status: (%d)", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if jsonErr != nil {
|
|
||||||
return nil, jsonErr
|
|
||||||
}
|
|
||||||
|
|
||||||
if response.Error != 0 {
|
|
||||||
return &response, &lastFMError{Code: response.Error, Message: response.Message}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) ArtistGetInfo(ctx context.Context, name string, mbid string) (*Artist, error) {
|
func (c *Client) ArtistGetInfo(ctx context.Context, name string, mbid string) (*Artist, error) {
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Add("method", "artist.getInfo")
|
params.Add("method", "artist.getInfo")
|
||||||
@ -111,3 +81,76 @@ func (c *Client) ArtistGetTopTracks(ctx context.Context, name string, mbid strin
|
|||||||
}
|
}
|
||||||
return &response.TopTracks, nil
|
return &response.TopTracks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetToken(ctx context.Context) (string, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("method", "auth.getToken")
|
||||||
|
c.sign(params)
|
||||||
|
response, err := c.makeRequest(params)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return response.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetSession(ctx context.Context, token string) (string, error) {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("method", "auth.getSession")
|
||||||
|
params.Add("token", token)
|
||||||
|
c.sign(params)
|
||||||
|
response, err := c.makeRequest(params)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return response.Session.Key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) makeRequest(params url.Values) (*Response, error) {
|
||||||
|
params.Add("format", "json")
|
||||||
|
params.Add("api_key", c.apiKey)
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", apiBaseUrl, nil)
|
||||||
|
req.URL.RawQuery = params.Encode()
|
||||||
|
|
||||||
|
resp, err := c.hc.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
decoder := json.NewDecoder(resp.Body)
|
||||||
|
|
||||||
|
var response Response
|
||||||
|
jsonErr := decoder.Decode(&response)
|
||||||
|
if resp.StatusCode != 200 && jsonErr != nil {
|
||||||
|
return nil, fmt.Errorf("last.fm http status: (%d)", resp.StatusCode)
|
||||||
|
}
|
||||||
|
if jsonErr != nil {
|
||||||
|
return nil, jsonErr
|
||||||
|
}
|
||||||
|
if response.Error != 0 {
|
||||||
|
return &response, &lastFMError{Code: response.Error, Message: response.Message}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) sign(params url.Values) {
|
||||||
|
// the parameters must be in order before hashing
|
||||||
|
keys := make([]string, 0, len(params))
|
||||||
|
for k := range params {
|
||||||
|
if utils.StringInSlice(k, []string{"format", "callback"}) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
msg := strings.Builder{}
|
||||||
|
for _, k := range keys {
|
||||||
|
msg.WriteString(k)
|
||||||
|
msg.WriteString(params[k][0])
|
||||||
|
}
|
||||||
|
msg.WriteString(c.secret)
|
||||||
|
hash := md5.Sum([]byte(msg.String()))
|
||||||
|
params.Add("api_sig", hex.EncodeToString(hash[:]))
|
||||||
|
}
|
||||||
|
@ -3,9 +3,12 @@ package lastfm
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/md5"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/navidrome/navidrome/tests"
|
"github.com/navidrome/navidrome/tests"
|
||||||
@ -19,7 +22,7 @@ var _ = Describe("Client", func() {
|
|||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
httpClient = &tests.FakeHttpClient{}
|
httpClient = &tests.FakeHttpClient{}
|
||||||
client = NewClient("API_KEY", "pt", httpClient)
|
client = NewClient("API_KEY", "SECRET", "pt", httpClient)
|
||||||
})
|
})
|
||||||
|
|
||||||
Describe("ArtistGetInfo", func() {
|
Describe("ArtistGetInfo", func() {
|
||||||
@ -27,7 +30,7 @@ var _ = Describe("Client", func() {
|
|||||||
f, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json")
|
f, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json")
|
||||||
httpClient.Res = http.Response{Body: f, StatusCode: 200}
|
httpClient.Res = http.Response{Body: f, StatusCode: 200}
|
||||||
|
|
||||||
artist, err := client.ArtistGetInfo(context.TODO(), "U2", "123")
|
artist, err := client.ArtistGetInfo(context.Background(), "U2", "123")
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
Expect(artist.Name).To(Equal("U2"))
|
Expect(artist.Name).To(Equal("U2"))
|
||||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&lang=pt&mbid=123&method=artist.getInfo"))
|
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&lang=pt&mbid=123&method=artist.getInfo"))
|
||||||
@ -39,7 +42,7 @@ var _ = Describe("Client", func() {
|
|||||||
StatusCode: 500,
|
StatusCode: 500,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := client.ArtistGetInfo(context.TODO(), "U2", "123")
|
_, err := client.ArtistGetInfo(context.Background(), "U2", "123")
|
||||||
Expect(err).To(MatchError("last.fm http status: (500)"))
|
Expect(err).To(MatchError("last.fm http status: (500)"))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -49,7 +52,7 @@ var _ = Describe("Client", func() {
|
|||||||
StatusCode: 400,
|
StatusCode: 400,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := client.ArtistGetInfo(context.TODO(), "U2", "123")
|
_, err := client.ArtistGetInfo(context.Background(), "U2", "123")
|
||||||
Expect(err).To(MatchError(&lastFMError{Code: 3, Message: "Invalid Method - No method with that name in this package"}))
|
Expect(err).To(MatchError(&lastFMError{Code: 3, Message: "Invalid Method - No method with that name in this package"}))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -59,14 +62,14 @@ var _ = Describe("Client", func() {
|
|||||||
StatusCode: 200,
|
StatusCode: 200,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := client.ArtistGetInfo(context.TODO(), "U2", "123")
|
_, err := client.ArtistGetInfo(context.Background(), "U2", "123")
|
||||||
Expect(err).To(MatchError(&lastFMError{Code: 6, Message: "The artist you supplied could not be found"}))
|
Expect(err).To(MatchError(&lastFMError{Code: 6, Message: "The artist you supplied could not be found"}))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("fails if HttpClient.Do() returns error", func() {
|
It("fails if HttpClient.Do() returns error", func() {
|
||||||
httpClient.Err = errors.New("generic error")
|
httpClient.Err = errors.New("generic error")
|
||||||
|
|
||||||
_, err := client.ArtistGetInfo(context.TODO(), "U2", "123")
|
_, err := client.ArtistGetInfo(context.Background(), "U2", "123")
|
||||||
Expect(err).To(MatchError("generic error"))
|
Expect(err).To(MatchError("generic error"))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -76,7 +79,7 @@ var _ = Describe("Client", func() {
|
|||||||
StatusCode: 200,
|
StatusCode: 200,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := client.ArtistGetInfo(context.TODO(), "U2", "123")
|
_, err := client.ArtistGetInfo(context.Background(), "U2", "123")
|
||||||
Expect(err).To(MatchError("invalid character '<' looking for beginning of value"))
|
Expect(err).To(MatchError("invalid character '<' looking for beginning of value"))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -87,7 +90,7 @@ var _ = Describe("Client", func() {
|
|||||||
f, _ := os.Open("tests/fixtures/lastfm.artist.getsimilar.json")
|
f, _ := os.Open("tests/fixtures/lastfm.artist.getsimilar.json")
|
||||||
httpClient.Res = http.Response{Body: f, StatusCode: 200}
|
httpClient.Res = http.Response{Body: f, StatusCode: 200}
|
||||||
|
|
||||||
similar, err := client.ArtistGetSimilar(context.TODO(), "U2", "123", 2)
|
similar, err := client.ArtistGetSimilar(context.Background(), "U2", "123", 2)
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
Expect(len(similar.Artists)).To(Equal(2))
|
Expect(len(similar.Artists)).To(Equal(2))
|
||||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&limit=2&mbid=123&method=artist.getSimilar"))
|
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&limit=2&mbid=123&method=artist.getSimilar"))
|
||||||
@ -99,10 +102,60 @@ var _ = Describe("Client", func() {
|
|||||||
f, _ := os.Open("tests/fixtures/lastfm.artist.gettoptracks.json")
|
f, _ := os.Open("tests/fixtures/lastfm.artist.gettoptracks.json")
|
||||||
httpClient.Res = http.Response{Body: f, StatusCode: 200}
|
httpClient.Res = http.Response{Body: f, StatusCode: 200}
|
||||||
|
|
||||||
top, err := client.ArtistGetTopTracks(context.TODO(), "U2", "123", 2)
|
top, err := client.ArtistGetTopTracks(context.Background(), "U2", "123", 2)
|
||||||
Expect(err).To(BeNil())
|
Expect(err).To(BeNil())
|
||||||
Expect(len(top.Track)).To(Equal(2))
|
Expect(len(top.Track)).To(Equal(2))
|
||||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&limit=2&mbid=123&method=artist.getTopTracks"))
|
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&limit=2&mbid=123&method=artist.getTopTracks"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Describe("GetToken", func() {
|
||||||
|
It("returns a token when the request is successful", func() {
|
||||||
|
httpClient.Res = http.Response{
|
||||||
|
Body: ioutil.NopCloser(bytes.NewBufferString(`{"token":"TOKEN"}`)),
|
||||||
|
StatusCode: 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
Expect(client.GetToken(context.Background())).To(Equal("TOKEN"))
|
||||||
|
queryParams := httpClient.SavedRequest.URL.Query()
|
||||||
|
Expect(queryParams.Get("method")).To(Equal("auth.getToken"))
|
||||||
|
Expect(queryParams.Get("format")).To(Equal("json"))
|
||||||
|
Expect(queryParams.Get("api_key")).To(Equal("API_KEY"))
|
||||||
|
Expect(queryParams.Get("api_sig")).ToNot(BeEmpty())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("GetSession", func() {
|
||||||
|
It("returns a session key when the request is successful", func() {
|
||||||
|
httpClient.Res = http.Response{
|
||||||
|
Body: ioutil.NopCloser(bytes.NewBufferString(`{"session":{"name":"Navidrome","key":"SESSION_KEY","subscriber":0}}`)),
|
||||||
|
StatusCode: 200,
|
||||||
|
}
|
||||||
|
|
||||||
|
Expect(client.GetSession(context.Background(), "TOKEN")).To(Equal("SESSION_KEY"))
|
||||||
|
queryParams := httpClient.SavedRequest.URL.Query()
|
||||||
|
Expect(queryParams.Get("method")).To(Equal("auth.getSession"))
|
||||||
|
Expect(queryParams.Get("format")).To(Equal("json"))
|
||||||
|
Expect(queryParams.Get("token")).To(Equal("TOKEN"))
|
||||||
|
Expect(queryParams.Get("api_key")).To(Equal("API_KEY"))
|
||||||
|
Expect(queryParams.Get("api_sig")).ToNot(BeEmpty())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("sign", func() {
|
||||||
|
It("adds an api_sig param with the signature", func() {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("d", "444")
|
||||||
|
params.Add("callback", "https://myserver.com")
|
||||||
|
params.Add("a", "111")
|
||||||
|
params.Add("format", "json")
|
||||||
|
params.Add("c", "333")
|
||||||
|
params.Add("b", "222")
|
||||||
|
client.sign(params)
|
||||||
|
Expect(params).To(HaveKey("api_sig"))
|
||||||
|
sig := params.Get("api_sig")
|
||||||
|
expected := fmt.Sprintf("%x", md5.Sum([]byte("a111b222c333d444SECRET")))
|
||||||
|
Expect(sig).To(Equal(expected))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -14,12 +14,13 @@ import (
|
|||||||
const (
|
const (
|
||||||
lastFMAgentName = "lastfm"
|
lastFMAgentName = "lastfm"
|
||||||
lastFMAPIKey = "9b94a5515ea66b2da3ec03c12300327e"
|
lastFMAPIKey = "9b94a5515ea66b2da3ec03c12300327e"
|
||||||
//lastFMAPISecret = "74cb6557cec7171d921af5d7d887c587" // Will be needed when implementing Scrobbling
|
lastFMAPISecret = "74cb6557cec7171d921af5d7d887c587" // nolint:gosec
|
||||||
)
|
)
|
||||||
|
|
||||||
type lastfmAgent struct {
|
type lastfmAgent struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
apiKey string
|
apiKey string
|
||||||
|
secret string
|
||||||
lang string
|
lang string
|
||||||
client *Client
|
client *Client
|
||||||
}
|
}
|
||||||
@ -28,14 +29,15 @@ func lastFMConstructor(ctx context.Context) agents.Interface {
|
|||||||
l := &lastfmAgent{
|
l := &lastfmAgent{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
lang: conf.Server.LastFM.Language,
|
lang: conf.Server.LastFM.Language,
|
||||||
|
apiKey: lastFMAPIKey,
|
||||||
|
secret: lastFMAPISecret,
|
||||||
}
|
}
|
||||||
if conf.Server.LastFM.ApiKey != "" {
|
if conf.Server.LastFM.ApiKey != "" {
|
||||||
l.apiKey = conf.Server.LastFM.ApiKey
|
l.apiKey = conf.Server.LastFM.ApiKey
|
||||||
} else {
|
l.secret = conf.Server.LastFM.Secret
|
||||||
l.apiKey = lastFMAPIKey
|
|
||||||
}
|
}
|
||||||
hc := utils.NewCachedHTTPClient(http.DefaultClient, consts.DefaultCachedHttpClientTTL)
|
hc := utils.NewCachedHTTPClient(http.DefaultClient, consts.DefaultCachedHttpClientTTL)
|
||||||
l.client = NewClient(l.apiKey, l.lang, hc)
|
l.client = NewClient(l.apiKey, l.secret, l.lang, hc)
|
||||||
return l
|
return l
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ var _ = Describe("lastfmAgent", func() {
|
|||||||
Describe("lastFMConstructor", func() {
|
Describe("lastFMConstructor", func() {
|
||||||
It("uses default api key and language if not configured", func() {
|
It("uses default api key and language if not configured", func() {
|
||||||
conf.Server.LastFM.ApiKey = ""
|
conf.Server.LastFM.ApiKey = ""
|
||||||
agent := lastFMConstructor(context.TODO())
|
agent := lastFMConstructor(context.Background())
|
||||||
Expect(agent.(*lastfmAgent).apiKey).To(Equal(lastFMAPIKey))
|
Expect(agent.(*lastfmAgent).apiKey).To(Equal(lastFMAPIKey))
|
||||||
Expect(agent.(*lastfmAgent).lang).To(Equal("en"))
|
Expect(agent.(*lastfmAgent).lang).To(Equal("en"))
|
||||||
})
|
})
|
||||||
@ -32,7 +32,7 @@ var _ = Describe("lastfmAgent", 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.Language = "pt"
|
conf.Server.LastFM.Language = "pt"
|
||||||
agent := lastFMConstructor(context.TODO())
|
agent := lastFMConstructor(context.Background())
|
||||||
Expect(agent.(*lastfmAgent).apiKey).To(Equal("123"))
|
Expect(agent.(*lastfmAgent).apiKey).To(Equal("123"))
|
||||||
Expect(agent.(*lastfmAgent).lang).To(Equal("pt"))
|
Expect(agent.(*lastfmAgent).lang).To(Equal("pt"))
|
||||||
})
|
})
|
||||||
@ -43,8 +43,8 @@ var _ = Describe("lastfmAgent", func() {
|
|||||||
var httpClient *tests.FakeHttpClient
|
var httpClient *tests.FakeHttpClient
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
httpClient = &tests.FakeHttpClient{}
|
httpClient = &tests.FakeHttpClient{}
|
||||||
client := NewClient("API_KEY", "pt", httpClient)
|
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
|
||||||
agent = lastFMConstructor(context.TODO()).(*lastfmAgent)
|
agent = lastFMConstructor(context.Background()).(*lastfmAgent)
|
||||||
agent.client = client
|
agent.client = client
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -101,8 +101,8 @@ var _ = Describe("lastfmAgent", func() {
|
|||||||
var httpClient *tests.FakeHttpClient
|
var httpClient *tests.FakeHttpClient
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
httpClient = &tests.FakeHttpClient{}
|
httpClient = &tests.FakeHttpClient{}
|
||||||
client := NewClient("API_KEY", "pt", httpClient)
|
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
|
||||||
agent = lastFMConstructor(context.TODO()).(*lastfmAgent)
|
agent = lastFMConstructor(context.Background()).(*lastfmAgent)
|
||||||
agent.client = client
|
agent.client = client
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -162,8 +162,8 @@ var _ = Describe("lastfmAgent", func() {
|
|||||||
var httpClient *tests.FakeHttpClient
|
var httpClient *tests.FakeHttpClient
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
httpClient = &tests.FakeHttpClient{}
|
httpClient = &tests.FakeHttpClient{}
|
||||||
client := NewClient("API_KEY", "pt", httpClient)
|
client := NewClient("API_KEY", "SECRET", "pt", httpClient)
|
||||||
agent = lastFMConstructor(context.TODO()).(*lastfmAgent)
|
agent = lastFMConstructor(context.Background()).(*lastfmAgent)
|
||||||
agent.client = client
|
agent.client = client
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -6,6 +6,8 @@ type Response struct {
|
|||||||
TopTracks TopTracks `json:"toptracks"`
|
TopTracks TopTracks `json:"toptracks"`
|
||||||
Error int `json:"error"`
|
Error int `json:"error"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
Session Session `json:"session"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Artist struct {
|
type Artist struct {
|
||||||
@ -59,3 +61,9 @@ type TopTracks struct {
|
|||||||
Track []Track `json:"track"`
|
Track []Track `json:"track"`
|
||||||
Attr Attr `json:"@attr"`
|
Attr Attr `json:"@attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Subscriber int `json:"subscriber"`
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user