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:
168
scrobble/lastfm/client.go
Normal file
168
scrobble/lastfm/client.go
Normal 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
|
||||||
|
}
|
||||||
161
scrobble/lastfm/client_test.go
Normal file
161
scrobble/lastfm/client_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
108
scrobble/lastfm/model.go
Normal 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"`
|
||||||
|
}
|
||||||
|
)
|
||||||
67
scrobble/lastfm/scrobbler.go
Normal file
67
scrobble/lastfm/scrobbler.go
Normal 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
|
||||||
|
}
|
||||||
36
scrobble/lastfm/testdata/artist_get_info_response.xml
vendored
Normal file
36
scrobble/lastfm/testdata/artist_get_info_response.xml
vendored
Normal 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>
|
||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"go.senan.xyz/gonic"
|
"go.senan.xyz/gonic"
|
||||||
"go.senan.xyz/gonic/db"
|
"go.senan.xyz/gonic/db"
|
||||||
"go.senan.xyz/gonic/podcasts"
|
"go.senan.xyz/gonic/podcasts"
|
||||||
|
"go.senan.xyz/gonic/scrobble/lastfm"
|
||||||
"go.senan.xyz/gonic/server/ctrladmin/adminui"
|
"go.senan.xyz/gonic/server/ctrladmin/adminui"
|
||||||
"go.senan.xyz/gonic/server/ctrlbase"
|
"go.senan.xyz/gonic/server/ctrlbase"
|
||||||
)
|
)
|
||||||
@@ -78,9 +79,10 @@ type Controller struct {
|
|||||||
template *template.Template
|
template *template.Template
|
||||||
sessDB *gormstore.Store
|
sessDB *gormstore.Store
|
||||||
Podcasts *podcasts.Podcasts
|
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.
|
tmpl, err := template.
|
||||||
New("layout").
|
New("layout").
|
||||||
Funcs(template.FuncMap(sprig.FuncMap())).
|
Funcs(template.FuncMap(sprig.FuncMap())).
|
||||||
@@ -98,6 +100,7 @@ func New(b *ctrlbase.Controller, sessDB *gormstore.Store, podcasts *podcasts.Pod
|
|||||||
template: tmpl,
|
template: tmpl,
|
||||||
sessDB: sessDB,
|
sessDB: sessDB,
|
||||||
Podcasts: podcasts,
|
Podcasts: podcasts,
|
||||||
|
lastfmClient: lastfmClient,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
|
|
||||||
"go.senan.xyz/gonic/db"
|
"go.senan.xyz/gonic/db"
|
||||||
"go.senan.xyz/gonic/scanner"
|
"go.senan.xyz/gonic/scanner"
|
||||||
"go.senan.xyz/gonic/scrobble/lastfm"
|
|
||||||
"go.senan.xyz/gonic/scrobble/listenbrainz"
|
"go.senan.xyz/gonic/scrobble/listenbrainz"
|
||||||
"go.senan.xyz/gonic/transcode"
|
"go.senan.xyz/gonic/transcode"
|
||||||
)
|
)
|
||||||
@@ -106,7 +105,7 @@ func (c *Controller) ServeLinkLastFMDo(r *http.Request) *Response {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return &Response{redirect: r.Referer(), flashW: []string{fmt.Sprintf("couldn't get secret: %v", err)}}
|
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 {
|
if err != nil {
|
||||||
return &Response{
|
return &Response{
|
||||||
redirect: "/admin/home",
|
redirect: "/admin/home",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"go.senan.xyz/gonic/jukebox"
|
"go.senan.xyz/gonic/jukebox"
|
||||||
"go.senan.xyz/gonic/podcasts"
|
"go.senan.xyz/gonic/podcasts"
|
||||||
"go.senan.xyz/gonic/scrobble"
|
"go.senan.xyz/gonic/scrobble"
|
||||||
|
"go.senan.xyz/gonic/scrobble/lastfm"
|
||||||
"go.senan.xyz/gonic/server/ctrlbase"
|
"go.senan.xyz/gonic/server/ctrlbase"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
||||||
@@ -48,6 +49,7 @@ type Controller struct {
|
|||||||
Scrobblers []scrobble.Scrobbler
|
Scrobblers []scrobble.Scrobbler
|
||||||
Podcasts *podcasts.Podcasts
|
Podcasts *podcasts.Podcasts
|
||||||
Transcoder transcode.Transcoder
|
Transcoder transcode.Transcoder
|
||||||
|
LastFMClient *lastfm.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type metaResponse struct {
|
type metaResponse struct {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
"go.senan.xyz/gonic/db"
|
"go.senan.xyz/gonic/db"
|
||||||
"go.senan.xyz/gonic/scrobble/lastfm"
|
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/params"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
|
||||||
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
|
"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
|
||||||
@@ -318,7 +317,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
|
|||||||
if apiKey == "" {
|
if apiKey == "" {
|
||||||
return sub
|
return sub
|
||||||
}
|
}
|
||||||
info, err := lastfm.ArtistGetInfo(apiKey, artist.Name)
|
info, err := c.LastFMClient.ArtistGetInfo(apiKey, artist.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return spec.NewError(0, "fetching artist info: %v", err)
|
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
|
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.SmallImageURL = url
|
||||||
sub.ArtistInfoTwo.MediumImageURL = url
|
sub.ArtistInfoTwo.MediumImageURL = url
|
||||||
sub.ArtistInfoTwo.LargeImageURL = url
|
sub.ArtistInfoTwo.LargeImageURL = url
|
||||||
@@ -348,7 +347,7 @@ func (c *Controller) ServeGetArtistInfoTwo(r *http.Request) *spec.Response {
|
|||||||
|
|
||||||
count := params.GetOrInt("count", 20)
|
count := params.GetOrInt("count", 20)
|
||||||
inclNotPresent := params.GetOrBool("includeNotPresent", false)
|
inclNotPresent := params.GetOrBool("includeNotPresent", false)
|
||||||
similarArtists, err := lastfm.ArtistGetSimilar(apiKey, artist.Name)
|
similarArtists, err := c.LastFMClient.ArtistGetSimilar(apiKey, artist.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return spec.NewError(0, "fetching artist similar: %v", err)
|
return spec.NewError(0, "fetching artist similar: %v", err)
|
||||||
}
|
}
|
||||||
@@ -542,7 +541,7 @@ func (c *Controller) ServeGetTopSongs(r *http.Request) *spec.Response {
|
|||||||
if apiKey == "" {
|
if apiKey == "" {
|
||||||
return spec.NewResponse()
|
return spec.NewResponse()
|
||||||
}
|
}
|
||||||
topTracks, err := lastfm.ArtistGetTopTracks(apiKey, artist.Name)
|
topTracks, err := c.LastFMClient.ArtistGetTopTracks(apiKey, artist.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return spec.NewError(0, "fetching artist top tracks: %v", err)
|
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")
|
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 {
|
if err != nil {
|
||||||
return spec.NewError(0, "fetching track similar tracks: %v", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
return spec.NewError(0, "fetching artist similar artists: %v", err)
|
return spec.NewError(0, "fetching artist similar artists: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,17 +97,23 @@ func New(opts Options) (*Server, error) {
|
|||||||
opts.CacheAudioPath,
|
opts.CacheAudioPath,
|
||||||
)
|
)
|
||||||
|
|
||||||
ctrlAdmin, err := ctrladmin.New(base, sessDB, podcast)
|
lastfmClient := lastfm.NewClient()
|
||||||
|
|
||||||
|
ctrlAdmin, err := ctrladmin.New(base, sessDB, podcast, lastfmClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create admin controller: %w", err)
|
return nil, fmt.Errorf("create admin controller: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctrlSubsonic := &ctrlsubsonic.Controller{
|
ctrlSubsonic := &ctrlsubsonic.Controller{
|
||||||
Controller: base,
|
Controller: base,
|
||||||
MusicPaths: opts.MusicPaths,
|
MusicPaths: opts.MusicPaths,
|
||||||
PodcastsPath: opts.PodcastPath,
|
PodcastsPath: opts.PodcastPath,
|
||||||
CacheAudioPath: opts.CacheAudioPath,
|
CacheAudioPath: opts.CacheAudioPath,
|
||||||
CoverCachePath: opts.CoverCachePath,
|
CoverCachePath: opts.CoverCachePath,
|
||||||
Scrobblers: []scrobble.Scrobbler{&lastfm.Scrobbler{DB: opts.DB}, listenbrainz.NewScrobbler()},
|
Scrobblers: []scrobble.Scrobbler{
|
||||||
|
lastfm.NewScrobbler(opts.DB, lastfmClient),
|
||||||
|
listenbrainz.NewScrobbler(),
|
||||||
|
},
|
||||||
Podcasts: podcast,
|
Podcasts: podcast,
|
||||||
Transcoder: cacheTranscoder,
|
Transcoder: cacheTranscoder,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user