add initial Last.FM tests (#329)

* Move model into separate file

* Separate Last.FM client and scrobbler

* Use separate Last.FM client and scrobbler

* Fix playcount attribute name

* Add initial test for Last.FM client
This commit is contained in:
Gregor Zurowski
2023-05-20 13:24:03 +02:00
committed by GitHub
parent 6144ac7979
commit dbcccdc811
12 changed files with 572 additions and 359 deletions

168
scrobble/lastfm/client.go Normal file
View File

@@ -0,0 +1,168 @@
package lastfm
import (
"crypto/md5"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"sort"
"github.com/andybalholm/cascadia"
"golang.org/x/net/html"
)
const (
baseURL = "https://ws.audioscrobbler.com/2.0/"
)
var (
ErrLastFM = errors.New("last.fm error")
//nolint:gochecknoglobals
artistOpenGraphQuery = cascadia.MustCompile(`html > head > meta[property="og:image"]`)
)
type Client struct {
httpClient *http.Client
}
func NewClient() *Client {
return &Client{
httpClient: http.DefaultClient,
}
}
func getParamSignature(params url.Values, secret string) string {
// the parameters must be in order before hashing
paramKeys := make([]string, 0, len(params))
for k := range params {
paramKeys = append(paramKeys, k)
}
sort.Strings(paramKeys)
toHash := ""
for _, k := range paramKeys {
toHash += k
toHash += params[k][0]
}
toHash += secret
hash := md5.Sum([]byte(toHash))
return hex.EncodeToString(hash[:])
}
func (c *Client) makeRequest(method string, params url.Values) (LastFM, error) {
req, _ := http.NewRequest(method, baseURL, nil)
req.URL.RawQuery = params.Encode()
resp, err := c.httpClient.Do(req)
if err != nil {
return LastFM{}, fmt.Errorf("get: %w", err)
}
defer resp.Body.Close()
decoder := xml.NewDecoder(resp.Body)
lastfm := LastFM{}
if err = decoder.Decode(&lastfm); err != nil {
respBytes, _ := httputil.DumpResponse(resp, true)
log.Printf("received bad lastfm response:\n%s", string(respBytes))
return LastFM{}, fmt.Errorf("decoding: %w", err)
}
if lastfm.Error.Code != 0 {
respBytes, _ := httputil.DumpResponse(resp, true)
log.Printf("received bad lastfm response:\n%s", string(respBytes))
return LastFM{}, fmt.Errorf("%v: %w", lastfm.Error.Value, ErrLastFM)
}
return lastfm, nil
}
func (c *Client) ArtistGetInfo(apiKey string, artistName string) (Artist, error) {
params := url.Values{}
params.Add("method", "artist.getInfo")
params.Add("api_key", apiKey)
params.Add("artist", artistName)
resp, err := c.makeRequest("GET", params)
if err != nil {
return Artist{}, fmt.Errorf("making artist GET: %w", err)
}
return resp.Artist, nil
}
func (c *Client) ArtistGetTopTracks(apiKey, artistName string) (TopTracks, error) {
params := url.Values{}
params.Add("method", "artist.getTopTracks")
params.Add("api_key", apiKey)
params.Add("artist", artistName)
resp, err := c.makeRequest("GET", params)
if err != nil {
return TopTracks{}, fmt.Errorf("making track GET: %w", err)
}
return resp.TopTracks, nil
}
func (c *Client) TrackGetSimilarTracks(apiKey string, artistName, trackName string) (SimilarTracks, error) {
params := url.Values{}
params.Add("method", "track.getSimilar")
params.Add("api_key", apiKey)
params.Add("track", trackName)
params.Add("artist", artistName)
resp, err := c.makeRequest("GET", params)
if err != nil {
return SimilarTracks{}, fmt.Errorf("making track GET: %w", err)
}
return resp.SimilarTracks, nil
}
func (c *Client) ArtistGetSimilar(apiKey string, artistName string) (SimilarArtists, error) {
params := url.Values{}
params.Add("method", "artist.getSimilar")
params.Add("api_key", apiKey)
params.Add("artist", artistName)
resp, err := c.makeRequest("GET", params)
if err != nil {
return SimilarArtists{}, fmt.Errorf("making similar artists GET: %w", err)
}
return resp.SimilarArtists, nil
}
func (c *Client) GetSession(apiKey, secret, token string) (string, error) {
params := url.Values{}
params.Add("method", "auth.getSession")
params.Add("api_key", apiKey)
params.Add("token", token)
params.Add("api_sig", getParamSignature(params, secret))
resp, err := c.makeRequest("GET", params)
if err != nil {
return "", fmt.Errorf("making session GET: %w", err)
}
return resp.Session.Key, nil
}
func (c *Client) StealArtistImage(artistURL string) (string, error) {
resp, err := http.Get(artistURL) //nolint:gosec
if err != nil {
return "", fmt.Errorf("get artist url: %w", err)
}
defer resp.Body.Close()
node, err := html.Parse(resp.Body)
if err != nil {
return "", fmt.Errorf("parse html: %w", err)
}
n := cascadia.Query(node, artistOpenGraphQuery)
if n == nil {
return "", nil
}
var imageURL string
for _, attr := range n.Attr {
if attr.Key == "content" {
imageURL = attr.Val
break
}
}
return imageURL, nil
}

View File

@@ -0,0 +1,161 @@
package lastfm
import (
"context"
"crypto/md5"
"crypto/tls"
_ "embed"
"encoding/xml"
"fmt"
"net"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/require"
)
func httpClientMock(handler http.Handler) (http.Client, func()) {
server := httptest.NewTLSServer(handler)
client := http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.Dial(network, server.Listener.Addr().String())
},
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec
},
},
}
return client, server.Close
}
//go:embed testdata/artist_get_info_response.xml
var artistGetInfoResponse string
func TestArtistGetInfo(t *testing.T) {
// arrange
require := require.New(t)
httpClient, shutdown := httpClientMock(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(http.MethodGet, r.Method)
require.Equal(url.Values{
"method": []string{"artist.getInfo"},
"api_key": []string{"apiKey1"},
"artist": []string{"Artist 1"},
}, r.URL.Query())
require.Equal("/2.0/", r.URL.Path)
require.Equal(baseURL, "https://"+r.Host+r.URL.Path)
w.WriteHeader(http.StatusOK)
w.Write([]byte(artistGetInfoResponse))
}))
defer shutdown()
client := Client{&httpClient}
// act
actual, err := client.ArtistGetInfo("apiKey1", "Artist 1")
// assert
require.NoError(err)
require.Equal(Artist{
XMLName: xml.Name{
Local: "artist",
},
Name: "Artist 1",
MBID: "366c1119-ec4f-4312-b729-a5637d148e3e",
Streamable: "0",
Stats: struct {
Listeners string `xml:"listeners"`
Playcount string `xml:"playcount"`
}{
Listeners: "1",
Playcount: "2",
},
URL: "https://www.last.fm/music/Artist+1",
Image: []ArtistImage{
{
Size: "small",
Text: "https://last.fm/artist-1-small.png",
},
},
Bio: ArtistBio{
Published: "13 May 2023, 00:24",
Summary: "Summary",
Content: "Content",
},
Similar: struct {
Artists []Artist `xml:"artist"`
}{
Artists: []Artist{
{
XMLName: xml.Name{
Local: "artist",
},
Name: "Similar Artist 1",
URL: "https://www.last.fm/music/Similar+Artist+1",
Image: []ArtistImage{
{
Size: "small",
Text: "https://last.fm/similar-artist-1-small.png",
},
},
},
},
},
Tags: struct {
Tag []ArtistTag `xml:"tag"`
}{
Tag: []ArtistTag{
{
Name: "tag1",
URL: "https://www.last.fm/tag/tag1",
},
},
},
}, actual)
}
func TestArtistGetInfo_clientRequestFails(t *testing.T) {
// arrange
require := require.New(t)
httpClient, shutdown := httpClientMock(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(http.MethodGet, r.Method)
require.Equal(url.Values{
"method": []string{"artist.getInfo"},
"api_key": []string{"apiKey1"},
"artist": []string{"Artist 1"},
}, r.URL.Query())
require.Equal("/2.0/", r.URL.Path)
require.Equal(baseURL, "https://"+r.Host+r.URL.Path)
w.WriteHeader(http.StatusInternalServerError)
}))
defer shutdown()
client := Client{&httpClient}
// act
actual, err := client.ArtistGetInfo("apiKey1", "Artist 1")
// assert
require.Error(err)
require.Zero(actual)
}
func TestGetParamSignature(t *testing.T) {
params := url.Values{}
params.Add("ccc", "CCC")
params.Add("bbb", "BBB")
params.Add("aaa", "AAA")
params.Add("ddd", "DDD")
actual := getParamSignature(params, "secret")
expected := fmt.Sprintf("%x", md5.Sum([]byte(
"aaaAAAbbbBBBcccCCCdddDDDsecret",
)))
if actual != expected {
t.Errorf("expected %x, got %s", expected, actual)
}
}

View File

@@ -1,313 +0,0 @@
package lastfm
import (
"crypto/md5"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"sort"
"strconv"
"time"
"github.com/andybalholm/cascadia"
"github.com/google/uuid"
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/scrobble"
"golang.org/x/net/html"
)
const (
baseURL = "https://ws.audioscrobbler.com/2.0/"
)
var (
ErrLastFM = errors.New("last.fm error")
)
type LastFM struct {
XMLName xml.Name `xml:"lfm"`
Status string `xml:"status,attr"`
Session Session `xml:"session"`
Error Error `xml:"error"`
Artist Artist `xml:"artist"`
TopTracks TopTracks `xml:"toptracks"`
SimilarTracks SimilarTracks `xml:"similartracks"`
SimilarArtists SimilarArtists `xml:"similarartists"`
}
type Session struct {
Name string `xml:"name"`
Key string `xml:"key"`
Subscriber uint `xml:"subscriber"`
}
type Error struct {
Code uint `xml:"code,attr"`
Value string `xml:",chardata"`
}
type SimilarArtist struct {
XMLName xml.Name `xml:"artist"`
Name string `xml:"name"`
MBID string `xml:"mbid"`
URL string `xml:"url"`
Image []struct {
Text string `xml:",chardata"`
Size string `xml:"size,attr"`
} `xml:"image"`
Streamable string `xml:"streamable"`
}
type ArtistImage struct {
Text string `xml:",chardata"`
Size string `xml:"size,attr"`
}
type Artist struct {
XMLName xml.Name `xml:"artist"`
Name string `xml:"name"`
MBID string `xml:"mbid"`
URL string `xml:"url"`
Image []ArtistImage `xml:"image"`
Streamable string `xml:"streamable"`
Stats struct {
Listeners string `xml:"listeners"`
Plays string `xml:"plays"`
} `xml:"stats"`
Similar struct {
Artists []Artist `xml:"artist"`
} `xml:"similar"`
Tags struct {
Tag []ArtistTag `xml:"tag"`
} `xml:"tags"`
Bio ArtistBio `xml:"bio"`
}
type ArtistTag struct {
Name string `xml:"name"`
URL string `xml:"url"`
}
type ArtistBio struct {
Published string `xml:"published"`
Summary string `xml:"summary"`
Content string `xml:"content"`
}
type TopTracks struct {
XMLName xml.Name `xml:"toptracks"`
Artist string `xml:"artist,attr"`
Tracks []Track `xml:"track"`
}
type SimilarTracks struct {
XMLName xml.Name `xml:"similartracks"`
Artist string `xml:"artist,attr"`
Track string `xml:"track,attr"`
Tracks []Track `xml:"track"`
}
type SimilarArtists struct {
XMLName xml.Name `xml:"similarartists"`
Artist string `xml:"artist,attr"`
Artists []Artist `xml:"artist"`
}
type Track struct {
Rank int `xml:"rank,attr"`
Tracks []Track `xml:"track"`
Name string `xml:"name"`
MBID string `xml:"mbid"`
PlayCount int `xml:"playcount"`
Listeners int `xml:"listeners"`
URL string `xml:"url"`
Image []struct {
Text string `xml:",chardata"`
Size string `xml:"size,attr"`
} `xml:"image"`
}
func getParamSignature(params url.Values, secret string) string {
// the parameters must be in order before hashing
paramKeys := make([]string, 0, len(params))
for k := range params {
paramKeys = append(paramKeys, k)
}
sort.Strings(paramKeys)
toHash := ""
for _, k := range paramKeys {
toHash += k
toHash += params[k][0]
}
toHash += secret
hash := md5.Sum([]byte(toHash))
return hex.EncodeToString(hash[:])
}
func makeRequest(method string, params url.Values) (LastFM, error) {
req, _ := http.NewRequest(method, baseURL, nil)
req.URL.RawQuery = params.Encode()
resp, err := http.DefaultClient.Do(req)
if err != nil {
return LastFM{}, fmt.Errorf("get: %w", err)
}
defer resp.Body.Close()
decoder := xml.NewDecoder(resp.Body)
lastfm := LastFM{}
if err = decoder.Decode(&lastfm); err != nil {
respBytes, _ := httputil.DumpResponse(resp, true)
log.Printf("received bad lastfm response:\n%s", string(respBytes))
return LastFM{}, fmt.Errorf("decoding: %w", err)
}
if lastfm.Error.Code != 0 {
respBytes, _ := httputil.DumpResponse(resp, true)
log.Printf("received bad lastfm response:\n%s", string(respBytes))
return LastFM{}, fmt.Errorf("%v: %w", lastfm.Error.Value, ErrLastFM)
}
return lastfm, nil
}
func ArtistGetInfo(apiKey string, artistName string) (Artist, error) {
params := url.Values{}
params.Add("method", "artist.getInfo")
params.Add("api_key", apiKey)
params.Add("artist", artistName)
resp, err := makeRequest("GET", params)
if err != nil {
return Artist{}, fmt.Errorf("making artist GET: %w", err)
}
return resp.Artist, nil
}
func ArtistGetTopTracks(apiKey, artistName string) (TopTracks, error) {
params := url.Values{}
params.Add("method", "artist.getTopTracks")
params.Add("api_key", apiKey)
params.Add("artist", artistName)
resp, err := makeRequest("GET", params)
if err != nil {
return TopTracks{}, fmt.Errorf("making track GET: %w", err)
}
return resp.TopTracks, nil
}
func TrackGetSimilarTracks(apiKey string, artistName, trackName string) (SimilarTracks, error) {
params := url.Values{}
params.Add("method", "track.getSimilar")
params.Add("api_key", apiKey)
params.Add("track", trackName)
params.Add("artist", artistName)
resp, err := makeRequest("GET", params)
if err != nil {
return SimilarTracks{}, fmt.Errorf("making track GET: %w", err)
}
return resp.SimilarTracks, nil
}
func ArtistGetSimilar(apiKey string, artistName string) (SimilarArtists, error) {
params := url.Values{}
params.Add("method", "artist.getSimilar")
params.Add("api_key", apiKey)
params.Add("artist", artistName)
resp, err := makeRequest("GET", params)
if err != nil {
return SimilarArtists{}, fmt.Errorf("making similar artists GET: %w", err)
}
return resp.SimilarArtists, nil
}
func GetSession(apiKey, secret, token string) (string, error) {
params := url.Values{}
params.Add("method", "auth.getSession")
params.Add("api_key", apiKey)
params.Add("token", token)
params.Add("api_sig", getParamSignature(params, secret))
resp, err := makeRequest("GET", params)
if err != nil {
return "", fmt.Errorf("making session GET: %w", err)
}
return resp.Session.Key, nil
}
type Scrobbler struct {
DB *db.DB
}
func (s *Scrobbler) Scrobble(user *db.User, track *db.Track, stamp time.Time, submission bool) error {
if user.LastFMSession == "" {
return nil
}
apiKey, err := s.DB.GetSetting("lastfm_api_key")
if err != nil {
return fmt.Errorf("get api key: %w", err)
}
secret, err := s.DB.GetSetting("lastfm_secret")
if err != nil {
return fmt.Errorf("get secret: %w", err)
}
params := url.Values{}
if submission {
params.Add("method", "track.Scrobble")
// last.fm wants the timestamp in seconds
params.Add("timestamp", strconv.Itoa(int(stamp.Unix())))
} else {
params.Add("method", "track.updateNowPlaying")
}
params.Add("api_key", apiKey)
params.Add("sk", user.LastFMSession)
params.Add("artist", track.TagTrackArtist)
params.Add("track", track.TagTitle)
params.Add("trackNumber", strconv.Itoa(track.TagTrackNumber))
params.Add("album", track.Album.TagTitle)
params.Add("albumArtist", track.Artist.Name)
params.Add("duration", strconv.Itoa(track.Length))
// make sure we provide a valid uuid, since some users may have an incorrect mbid in their tags
if _, err := uuid.Parse(track.TagBrainzID); err == nil {
params.Add("mbid", track.TagBrainzID)
}
params.Add("api_sig", getParamSignature(params, secret))
_, err = makeRequest("POST", params)
return err
}
var _ scrobble.Scrobbler = (*Scrobbler)(nil)
//nolint:gochecknoglobals
var artistOpenGraphQuery = cascadia.MustCompile(`html > head > meta[property="og:image"]`)
func StealArtistImage(artistURL string) (string, error) {
resp, err := http.Get(artistURL) //nolint:gosec
if err != nil {
return "", fmt.Errorf("get artist url: %w", err)
}
defer resp.Body.Close()
node, err := html.Parse(resp.Body)
if err != nil {
return "", fmt.Errorf("parse html: %w", err)
}
n := cascadia.Query(node, artistOpenGraphQuery)
if n == nil {
return "", nil
}
var imageURL string
for _, attr := range n.Attr {
if attr.Key == "content" {
imageURL = attr.Val
break
}
}
return imageURL, nil
}

View File

@@ -1,23 +0,0 @@
package lastfm
import (
"crypto/md5"
"fmt"
"net/url"
"testing"
)
func TestGetParamSignature(t *testing.T) {
params := url.Values{}
params.Add("ccc", "CCC")
params.Add("bbb", "BBB")
params.Add("aaa", "AAA")
params.Add("ddd", "DDD")
actual := getParamSignature(params, "secret")
expected := fmt.Sprintf("%x", md5.Sum([]byte(
"aaaAAAbbbBBBcccCCCdddDDDsecret",
)))
if actual != expected {
t.Errorf("expected %x, got %s", expected, actual)
}
}

108
scrobble/lastfm/model.go Normal file
View File

@@ -0,0 +1,108 @@
package lastfm
import "encoding/xml"
type (
LastFM struct {
XMLName xml.Name `xml:"lfm"`
Status string `xml:"status,attr"`
Session Session `xml:"session"`
Error Error `xml:"error"`
Artist Artist `xml:"artist"`
TopTracks TopTracks `xml:"toptracks"`
SimilarTracks SimilarTracks `xml:"similartracks"`
SimilarArtists SimilarArtists `xml:"similarartists"`
}
Session struct {
Name string `xml:"name"`
Key string `xml:"key"`
Subscriber uint `xml:"subscriber"`
}
Error struct {
Code uint `xml:"code,attr"`
Value string `xml:",chardata"`
}
SimilarArtist struct {
XMLName xml.Name `xml:"artist"`
Name string `xml:"name"`
MBID string `xml:"mbid"`
URL string `xml:"url"`
Image []struct {
Text string `xml:",chardata"`
Size string `xml:"size,attr"`
} `xml:"image"`
Streamable string `xml:"streamable"`
}
ArtistImage struct {
Text string `xml:",chardata"`
Size string `xml:"size,attr"`
}
Artist struct {
XMLName xml.Name `xml:"artist"`
Name string `xml:"name"`
MBID string `xml:"mbid"`
URL string `xml:"url"`
Image []ArtistImage `xml:"image"`
Streamable string `xml:"streamable"`
Stats struct {
Listeners string `xml:"listeners"`
Playcount string `xml:"playcount"`
} `xml:"stats"`
Similar struct {
Artists []Artist `xml:"artist"`
} `xml:"similar"`
Tags struct {
Tag []ArtistTag `xml:"tag"`
} `xml:"tags"`
Bio ArtistBio `xml:"bio"`
}
ArtistTag struct {
Name string `xml:"name"`
URL string `xml:"url"`
}
ArtistBio struct {
Published string `xml:"published"`
Summary string `xml:"summary"`
Content string `xml:"content"`
}
TopTracks struct {
XMLName xml.Name `xml:"toptracks"`
Artist string `xml:"artist,attr"`
Tracks []Track `xml:"track"`
}
SimilarTracks struct {
XMLName xml.Name `xml:"similartracks"`
Artist string `xml:"artist,attr"`
Track string `xml:"track,attr"`
Tracks []Track `xml:"track"`
}
SimilarArtists struct {
XMLName xml.Name `xml:"similarartists"`
Artist string `xml:"artist,attr"`
Artists []Artist `xml:"artist"`
}
Track struct {
Rank int `xml:"rank,attr"`
Tracks []Track `xml:"track"`
Name string `xml:"name"`
MBID string `xml:"mbid"`
PlayCount int `xml:"playcount"`
Listeners int `xml:"listeners"`
URL string `xml:"url"`
Image []struct {
Text string `xml:",chardata"`
Size string `xml:"size,attr"`
} `xml:"image"`
}
)

View File

@@ -0,0 +1,67 @@
package lastfm
import (
"fmt"
"net/url"
"strconv"
"time"
"github.com/google/uuid"
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/scrobble"
)
type Scrobbler struct {
db *db.DB
client *Client
}
var _ scrobble.Scrobbler = (*Scrobbler)(nil)
func NewScrobbler(db *db.DB, client *Client) *Scrobbler {
return &Scrobbler{
db: db,
client: client,
}
}
func (s *Scrobbler) Scrobble(user *db.User, track *db.Track, stamp time.Time, submission bool) error {
if user.LastFMSession == "" {
return nil
}
apiKey, err := s.db.GetSetting("lastfm_api_key")
if err != nil {
return fmt.Errorf("get api key: %w", err)
}
secret, err := s.db.GetSetting("lastfm_secret")
if err != nil {
return fmt.Errorf("get secret: %w", err)
}
params := url.Values{}
if submission {
params.Add("method", "track.Scrobble")
// last.fm wants the timestamp in seconds
params.Add("timestamp", strconv.Itoa(int(stamp.Unix())))
} else {
params.Add("method", "track.updateNowPlaying")
}
params.Add("api_key", apiKey)
params.Add("sk", user.LastFMSession)
params.Add("artist", track.TagTrackArtist)
params.Add("track", track.TagTitle)
params.Add("trackNumber", strconv.Itoa(track.TagTrackNumber))
params.Add("album", track.Album.TagTitle)
params.Add("albumArtist", track.Artist.Name)
params.Add("duration", strconv.Itoa(track.Length))
// make sure we provide a valid uuid, since some users may have an incorrect mbid in their tags
if _, err := uuid.Parse(track.TagBrainzID); err == nil {
params.Add("mbid", track.TagBrainzID)
}
params.Add("api_sig", getParamSignature(params, secret))
_, err = s.client.makeRequest("POST", params)
return err
}

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<lfm status="ok">
<artist>
<name>Artist 1</name>
<mbid>366c1119-ec4f-4312-b729-a5637d148e3e</mbid>
<url>https://www.last.fm/music/Artist+1</url>
<image size="small">https://last.fm/artist-1-small.png</image>
<streamable>0</streamable>
<ontour>0</ontour>
<stats>
<listeners>1</listeners>
<playcount>2</playcount>
</stats>
<similar>
<artist>
<name>Similar Artist 1</name>
<url>https://www.last.fm/music/Similar+Artist+1</url>
<image size="small">https://last.fm/similar-artist-1-small.png</image>
</artist>
</similar>
<tags>
<tag>
<name>tag1</name>
<url>https://www.last.fm/tag/tag1</url>
</tag>
</tags>
<bio>
<links>
<link rel="original" href="https://last.fm/music/Artist+1/+wiki"></link>
</links>
<published>13 May 2023, 00:24</published>
<summary>Summary</summary>
<content>Content</content>
</bio>
</artist>
</lfm>

View File

@@ -24,6 +24,7 @@ import (
"go.senan.xyz/gonic"
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/podcasts"
"go.senan.xyz/gonic/scrobble/lastfm"
"go.senan.xyz/gonic/server/ctrladmin/adminui"
"go.senan.xyz/gonic/server/ctrlbase"
)
@@ -74,13 +75,14 @@ func funcMap() template.FuncMap {
type Controller struct {
*ctrlbase.Controller
buffPool *bpool.BufferPool
template *template.Template
sessDB *gormstore.Store
Podcasts *podcasts.Podcasts
buffPool *bpool.BufferPool
template *template.Template
sessDB *gormstore.Store
Podcasts *podcasts.Podcasts
lastfmClient *lastfm.Client
}
func New(b *ctrlbase.Controller, sessDB *gormstore.Store, podcasts *podcasts.Podcasts) (*Controller, error) {
func New(b *ctrlbase.Controller, sessDB *gormstore.Store, podcasts *podcasts.Podcasts, lastfmClient *lastfm.Client) (*Controller, error) {
tmpl, err := template.
New("layout").
Funcs(template.FuncMap(sprig.FuncMap())).
@@ -93,11 +95,12 @@ func New(b *ctrlbase.Controller, sessDB *gormstore.Store, podcasts *podcasts.Pod
return nil, fmt.Errorf("build template: %w", err)
}
return &Controller{
Controller: b,
buffPool: bpool.NewBufferPool(64),
template: tmpl,
sessDB: sessDB,
Podcasts: podcasts,
Controller: b,
buffPool: bpool.NewBufferPool(64),
template: tmpl,
sessDB: sessDB,
Podcasts: podcasts,
lastfmClient: lastfmClient,
}, nil
}

View File

@@ -20,7 +20,6 @@ import (
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/scanner"
"go.senan.xyz/gonic/scrobble/lastfm"
"go.senan.xyz/gonic/scrobble/listenbrainz"
"go.senan.xyz/gonic/transcode"
)
@@ -106,7 +105,7 @@ func (c *Controller) ServeLinkLastFMDo(r *http.Request) *Response {
if err != nil {
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("couldn't get secret: %v", err)}}
}
sessionKey, err := lastfm.GetSession(apiKey, secret, token)
sessionKey, err := c.lastfmClient.GetSession(apiKey, secret, token)
if err != nil {
return &Response{
redirect: "/admin/home",

View File

@@ -12,6 +12,7 @@ import (
"go.senan.xyz/gonic/jukebox"
"go.senan.xyz/gonic/podcasts"
"go.senan.xyz/gonic/scrobble"
"go.senan.xyz/gonic/scrobble/lastfm"
"go.senan.xyz/gonic/server/ctrlbase"
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
@@ -48,6 +49,7 @@ type Controller struct {
Scrobblers []scrobble.Scrobbler
Podcasts *podcasts.Podcasts
Transcoder transcode.Transcoder
LastFMClient *lastfm.Client
}
type metaResponse struct {

View File

@@ -13,7 +13,6 @@ import (
"github.com/jinzhu/gorm"
"go.senan.xyz/gonic/db"
"go.senan.xyz/gonic/scrobble/lastfm"
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
@@ -318,7 +317,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
if apiKey == "" {
return sub
}
info, err := lastfm.ArtistGetInfo(apiKey, artist.Name)
info, err := c.LastFMClient.ArtistGetInfo(apiKey, artist.Name)
if err != nil {
return spec.NewError(0, "fetching artist info: %v", err)
}
@@ -338,7 +337,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
sub.ArtistInfoTwo.LargeImageURL = image.Text
}
}
if url, _ := lastfm.StealArtistImage(info.URL); url != "" {
if url, _ := c.LastFMClient.StealArtistImage(info.URL); url != "" {
sub.ArtistInfoTwo.SmallImageURL = url
sub.ArtistInfoTwo.MediumImageURL = url
sub.ArtistInfoTwo.LargeImageURL = url
@@ -348,7 +347,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
count := params.GetOrInt("count", 20)
inclNotPresent := params.GetOrBool("includeNotPresent", false)
similarArtists, err := lastfm.ArtistGetSimilar(apiKey, artist.Name)
similarArtists, err := c.LastFMClient.ArtistGetSimilar(apiKey, artist.Name)
if err != nil {
return spec.NewError(0, "fetching artist similar: %v", err)
}
@@ -542,7 +541,7 @@ func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response {
if apiKey == "" {
return spec.NewResponse()
}
topTracks, err := lastfm.ArtistGetTopTracks(apiKey, artist.Name)
topTracks, err := c.LastFMClient.ArtistGetTopTracks(apiKey, artist.Name)
if err != nil {
return spec.NewError(0, "fetching artist top tracks: %v", err)
}
@@ -610,7 +609,7 @@ func (c *Controller) ServeGetSimilarSongs(r *http.Request) *spec.Response {
return spec.NewError(10, "couldn't find a track with that id")
}
similarTracks, err := lastfm.TrackGetSimilarTracks(apiKey, track.Artist.Name, track.TagTitle)
similarTracks, err := c.LastFMClient.TrackGetSimilarTracks(apiKey, track.Artist.Name, track.TagTitle)
if err != nil {
return spec.NewError(0, "fetching track similar tracks: %v", err)
}
@@ -680,7 +679,7 @@ func (c *Controller) ServeGetSimilarSongsTwo(r *http.Request) *spec.Response {
return spec.NewError(0, "artist with id `%s` not found", id)
}
similarArtists, err := lastfm.ArtistGetSimilar(apiKey, artist.Name)
similarArtists, err := c.LastFMClient.ArtistGetSimilar(apiKey, artist.Name)
if err != nil {
return spec.NewError(0, "fetching artist similar artists: %v", err)
}

View File

@@ -97,19 +97,25 @@ func New(opts Options) (*Server, error) {
opts.CacheAudioPath,
)
ctrlAdmin, err := ctrladmin.New(base, sessDB, podcast)
lastfmClient := lastfm.NewClient()
ctrlAdmin, err := ctrladmin.New(base, sessDB, podcast, lastfmClient)
if err != nil {
return nil, fmt.Errorf("create admin controller: %w", err)
}
ctrlSubsonic := &ctrlsubsonic.Controller{
Controller: base,
MusicPaths: opts.MusicPaths,
PodcastsPath: opts.PodcastPath,
CacheAudioPath: opts.CacheAudioPath,
CoverCachePath: opts.CoverCachePath,
Scrobblers: []scrobble.Scrobbler{&lastfm.Scrobbler{DB: opts.DB}, listenbrainz.NewScrobbler()},
Podcasts: podcast,
Transcoder: cacheTranscoder,
Scrobblers: []scrobble.Scrobbler{
lastfm.NewScrobbler(opts.DB, lastfmClient),
listenbrainz.NewScrobbler(),
},
Podcasts: podcast,
Transcoder: cacheTranscoder,
}
setupMisc(r, base)