Spotify Link-Through & Navidrome Logo Overlay #15

Merged
Woahai321 merged 19 commits from main into main 2026-03-04 10:04:03 -07:00
10 changed files with 119 additions and 117 deletions
Showing only changes of commit 375dd85b15 - Show all commits
+12 -8
View File
@@ -84,17 +84,21 @@ func uploadToUguu(imageData []byte, contentType string) (string, error) {
body = append(body, imageData...)
body = append(body, []byte(fmt.Sprintf("\r\n--%s--\r\n", boundary))...)
req := pdk.NewHTTPRequest(pdk.MethodPost, "https://uguu.se/upload")
req.SetHeader("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", boundary))
req.SetBody(body)
resp := req.Send()
if resp.Status() >= 400 {
return "", fmt.Errorf("uguu.se upload failed: HTTP %d", resp.Status())
resp, err := host.HTTPSend(host.HTTPRequest{
Method: "POST",
URL: "https://uguu.se/upload",
Headers: map[string]string{"Content-Type": fmt.Sprintf("multipart/form-data; boundary=%s", boundary)},
Body: body,
})
if err != nil {
return "", fmt.Errorf("uguu.se upload failed: %w", err)
}
if resp.StatusCode >= 400 {
return "", fmt.Errorf("uguu.se upload failed: HTTP %d", resp.StatusCode)
}
var result uguuResponse
if err := json.Unmarshal(resp.Body(), &result); err != nil {
if err := json.Unmarshal(resp.Body, &result); err != nil {
return "", fmt.Errorf("failed to parse uguu.se response: %w", err)
}
+8 -7
View File
@@ -20,6 +20,8 @@ var _ = Describe("getImageURL", func() {
host.ArtworkMock.Calls = nil
host.SubsonicAPIMock.ExpectedCalls = nil
host.SubsonicAPIMock.Calls = nil
host.HTTPMock.ExpectedCalls = nil
host.HTTPMock.Calls = nil
pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe()
})
@@ -71,10 +73,9 @@ var _ = Describe("getImageURL", func() {
Return("image/jpeg", imageData, nil)
// Mock uguu.se HTTP upload
uguuReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://uguu.se/upload").Return(uguuReq)
pdk.PDKMock.On("Send", uguuReq).Return(pdk.NewStubHTTPResponse(200, nil,
[]byte(`{"success":true,"files":[{"url":"https://a.uguu.se/uploaded.jpg"}]}`)))
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.URL == "https://uguu.se/upload"
})).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`{"success":true,"files":[{"url":"https://a.uguu.se/uploaded.jpg"}]}`)}, nil)
// Mock cache set
host.CacheMock.On("SetString", "uguu.artwork.track1", "https://a.uguu.se/uploaded.jpg", int64(9000)).Return(nil)
@@ -98,9 +99,9 @@ var _ = Describe("getImageURL", func() {
host.SubsonicAPIMock.On("CallRaw", "/getCoverArt?u=testuser&id=track1&size=300").
Return("image/jpeg", []byte("fake-image-data"), nil)
uguuReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://uguu.se/upload").Return(uguuReq)
pdk.PDKMock.On("Send", uguuReq).Return(pdk.NewStubHTTPResponse(500, nil, []byte(`{"success":false}`)))
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.URL == "https://uguu.se/upload"
})).Return(&host.HTTPResponse{StatusCode: 500, Body: []byte(`{"success":false}`)}, nil)
url := getImageURL("testuser", "track1")
Expect(url).To(BeEmpty())
+1 -1
View File
@@ -3,7 +3,7 @@ module discord-rich-presence
go 1.25
require (
github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260207182358-29f98b889b11
github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260224182233-40719d928320
github.com/onsi/ginkgo/v2 v2.28.1
github.com/onsi/gomega v1.39.1
github.com/stretchr/testify v1.11.1
+2 -2
View File
@@ -30,8 +30,8 @@ github.com/maruel/natural v1.3.0 h1:VsmCsBmEyrR46RomtgHs5hbKADGRVtliHTyCOLFBpsg=
github.com/maruel/natural v1.3.0/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE=
github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A=
github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260207182358-29f98b889b11 h1:VE4bqzkS6apWDtco9hAGdThFttjbYoLR0DEILAGDyyc=
github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260207182358-29f98b889b11/go.mod h1:HijQ0Z0OeEa6LIwUJh6H9WqAptye096jHazmKXf+YV4=
github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260224182233-40719d928320 h1:TVn0Jv9Xd4aoyTbBoSMAv38Mfh8lWX/kMP2au2KX1cQ=
github.com/navidrome/navidrome/plugins/pdk/go v0.0.0-20260224182233-40719d928320/go.mod h1:HijQ0Z0OeEa6LIwUJh6H9WqAptye096jHazmKXf+YV4=
github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=
github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
+10 -12
View File
@@ -33,6 +33,8 @@ var _ = Describe("discordPlugin", func() {
host.ArtworkMock.Calls = nil
host.SubsonicAPIMock.ExpectedCalls = nil
host.SubsonicAPIMock.Calls = nil
host.HTTPMock.ExpectedCalls = nil
host.HTTPMock.Calls = nil
})
Describe("getConfig", func() {
@@ -128,9 +130,9 @@ var _ = Describe("discordPlugin", func() {
// Mock HTTP GET request for gateway discovery
gatewayResp := []byte(`{"url":"wss://gateway.discord.gg"}`)
gatewayReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodGet, "https://discord.com/api/gateway").Return(gatewayReq).Once()
pdk.PDKMock.On("Send", gatewayReq).Return(pdk.NewStubHTTPResponse(200, nil, gatewayResp)).Once()
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.Method == "GET" && req.URL == "https://discord.com/api/gateway"
})).Return(&host.HTTPResponse{StatusCode: 200, Body: gatewayResp}, nil)
// Mock WebSocket connection
host.WebSocketMock.On("Connect", mock.MatchedBy(func(url string) bool {
@@ -148,9 +150,7 @@ var _ = Describe("discordPlugin", func() {
host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil)
// Mock HTTP POST requests (Discord external assets API)
postReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(postReq)
pdk.PDKMock.On("Send", postReq).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`{}`)))
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`{}`)}, nil)
// Schedule clear activity callback
host.SchedulerMock.On("ScheduleOneTime", mock.Anything, payloadClearActivity, "testuser-clear").Return("testuser-clear", nil)
@@ -180,9 +180,9 @@ var _ = Describe("discordPlugin", func() {
// Connect mocks
host.CacheMock.On("GetInt", "discord.seq.testuser").Return(int64(0), false, errors.New("not found"))
gatewayResp := []byte(`{"url":"wss://gateway.discord.gg"}`)
gatewayReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodGet, "https://discord.com/api/gateway").Return(gatewayReq).Once()
pdk.PDKMock.On("Send", gatewayReq).Return(pdk.NewStubHTTPResponse(200, nil, gatewayResp)).Once()
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.Method == "GET" && req.URL == "https://discord.com/api/gateway"
})).Return(&host.HTTPResponse{StatusCode: 200, Body: gatewayResp}, nil)
host.WebSocketMock.On("Connect", mock.MatchedBy(func(url string) bool {
return strings.Contains(url, "gateway.discord.gg")
}), mock.Anything, "testuser").Return("testuser", nil)
@@ -199,9 +199,7 @@ var _ = Describe("discordPlugin", func() {
host.CacheMock.On("GetString", discordImageKey).Return("", false, nil)
host.CacheMock.On("SetString", discordImageKey, mock.Anything, mock.Anything).Return(nil)
host.ArtworkMock.On("GetTrackUrl", "track1", int32(300)).Return("https://example.com/art.jpg", nil)
postReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(postReq)
pdk.PDKMock.On("Send", postReq).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`{}`)))
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`{}`)}, nil)
host.SchedulerMock.On("ScheduleOneTime", mock.Anything, payloadClearActivity, "testuser-clear").Return("testuser-clear", nil)
err := plugin.NowPlaying(scrobbler.NowPlayingRequest{
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"strings"
"testing"
"github.com/navidrome/navidrome/plugins/pdk/go/host"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/mock"
@@ -17,6 +18,6 @@ func TestDiscordPlugin(t *testing.T) {
// Shared matchers for tighter mock expectations across all test files.
var (
discordImageKey = mock.MatchedBy(func(key string) bool { return strings.HasPrefix(key, "discord.image.") })
externalAssetsURL = mock.MatchedBy(func(url string) bool { return strings.Contains(url, "external-assets") })
externalAssetsReq = mock.MatchedBy(func(req host.HTTPRequest) bool { return strings.Contains(req.URL, "external-assets") })
spotifyURLKey = mock.MatchedBy(func(key string) bool { return strings.HasPrefix(key, "spotify.url.") })
)
+24 -14
View File
@@ -147,18 +147,22 @@ func (r *discordRPC) processImage(imageURL, clientID, token string, ttl int64) (
// Process via Discord API
body := fmt.Sprintf(`{"urls":[%q]}`, imageURL)
req := pdk.NewHTTPRequest(pdk.MethodPost, fmt.Sprintf("https://discord.com/api/v9/applications/%s/external-assets", clientID))
req.SetHeader("Authorization", token)
req.SetHeader("Content-Type", "application/json")
req.SetBody([]byte(body))
resp := req.Send()
if resp.Status() >= 400 {
return "", fmt.Errorf("failed to process image: HTTP %d", resp.Status())
resp, err := host.HTTPSend(host.HTTPRequest{
Method: "POST",
URL: fmt.Sprintf("https://discord.com/api/v9/applications/%s/external-assets", clientID),
Headers: map[string]string{"Authorization": token, "Content-Type": "application/json"},
Body: []byte(body),
})
if err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("HTTP request failed for image processing: %v", err))
return "", fmt.Errorf("failed to process image: %w", err)
}
if resp.StatusCode >= 400 {
return "", fmt.Errorf("failed to process image: HTTP %d", resp.StatusCode)
}
var data []map[string]string
if err := json.Unmarshal(resp.Body(), &data); err != nil {
if err := json.Unmarshal(resp.Body, &data); err != nil {
return "", fmt.Errorf("failed to unmarshal image response: %w", err)
}
@@ -257,14 +261,20 @@ func (r *discordRPC) sendMessage(username string, opCode int, payload any) error
// getDiscordGateway retrieves the Discord gateway URL.
func (r *discordRPC) getDiscordGateway() (string, error) {
req := pdk.NewHTTPRequest(pdk.MethodGet, "https://discord.com/api/gateway")
resp := req.Send()
if resp.Status() != 200 {
return "", fmt.Errorf("failed to get Discord gateway: HTTP %d", resp.Status())
resp, err := host.HTTPSend(host.HTTPRequest{
Method: "GET",
URL: "https://discord.com/api/gateway",
})
if err != nil {
pdk.Log(pdk.LogWarn, fmt.Sprintf("HTTP request failed for Discord gateway: %v", err))
return "", fmt.Errorf("failed to get Discord gateway: %w", err)
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("failed to get Discord gateway: HTTP %d", resp.StatusCode)
}
var result map[string]string
if err := json.Unmarshal(resp.Body(), &result); err != nil {
if err := json.Unmarshal(resp.Body, &result); err != nil {
return "", fmt.Errorf("failed to parse Discord gateway response: %w", err)
}
return result["url"], nil
+15 -35
View File
@@ -25,6 +25,8 @@ var _ = Describe("discordRPC", func() {
host.WebSocketMock.Calls = nil
host.SchedulerMock.ExpectedCalls = nil
host.SchedulerMock.Calls = nil
host.HTTPMock.ExpectedCalls = nil
host.HTTPMock.Calls = nil
})
Describe("sendMessage", func() {
@@ -81,9 +83,9 @@ var _ = Describe("discordRPC", func() {
// Mock HTTP GET request for gateway discovery
gatewayResp := []byte(`{"url":"wss://gateway.discord.gg"}`)
httpReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodGet, "https://discord.com/api/gateway").Return(httpReq)
pdk.PDKMock.On("Send", mock.Anything).Return(pdk.NewStubHTTPResponse(200, nil, gatewayResp))
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.Method == "GET" && req.URL == "https://discord.com/api/gateway"
})).Return(&host.HTTPResponse{StatusCode: 200, Body: gatewayResp}, nil)
// Mock WebSocket connection
host.WebSocketMock.On("Connect", mock.MatchedBy(func(url string) bool {
@@ -265,9 +267,7 @@ var _ = Describe("discordRPC", func() {
return val == "mp:external/new-asset"
}), int64(imageCacheTTL)).Return(nil)
httpReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(httpReq)
pdk.PDKMock.On("Send", mock.Anything).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`[{"external_asset_path":"external/new-asset"}]`)))
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`[{"external_asset_path":"external/new-asset"}]`)}, nil)
result, err := r.processImage("https://example.com/art.jpg", "client123", "token123", imageCacheTTL)
Expect(err).ToNot(HaveOccurred())
@@ -277,9 +277,7 @@ var _ = Describe("discordRPC", func() {
It("returns error on HTTP failure", func() {
host.CacheMock.On("GetString", discordImageKey).Return("", false, nil)
httpReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(httpReq)
pdk.PDKMock.On("Send", mock.Anything).Return(pdk.NewStubHTTPResponse(500, nil, []byte(`error`)))
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 500, Body: []byte(`error`)}, nil)
_, err := r.processImage("https://example.com/art.jpg", "client123", "token123", imageCacheTTL)
Expect(err).To(HaveOccurred())
@@ -289,9 +287,7 @@ var _ = Describe("discordRPC", func() {
It("returns error on unmarshal failure", func() {
host.CacheMock.On("GetString", discordImageKey).Return("", false, nil)
httpReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(httpReq)
pdk.PDKMock.On("Send", mock.Anything).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`{"not":"an-array"}`)))
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`{"not":"an-array"}`)}, nil)
_, err := r.processImage("https://example.com/art.jpg", "client123", "token123", imageCacheTTL)
Expect(err).To(HaveOccurred())
@@ -301,9 +297,7 @@ var _ = Describe("discordRPC", func() {
It("returns error on empty response array", func() {
host.CacheMock.On("GetString", discordImageKey).Return("", false, nil)
httpReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(httpReq)
pdk.PDKMock.On("Send", mock.Anything).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`[]`)))
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`[]`)}, nil)
_, err := r.processImage("https://example.com/art.jpg", "client123", "token123", imageCacheTTL)
Expect(err).To(HaveOccurred())
@@ -313,9 +307,7 @@ var _ = Describe("discordRPC", func() {
It("returns error on empty external_asset_path", func() {
host.CacheMock.On("GetString", discordImageKey).Return("", false, nil)
httpReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(httpReq)
pdk.PDKMock.On("Send", mock.Anything).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`[{"external_asset_path":""}]`)))
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`[{"external_asset_path":""}]`)}, nil)
_, err := r.processImage("https://example.com/art.jpg", "client123", "token123", imageCacheTTL)
Expect(err).To(HaveOccurred())
@@ -332,9 +324,7 @@ var _ = Describe("discordRPC", func() {
host.CacheMock.On("GetString", discordImageKey).Return("", false, nil)
host.CacheMock.On("SetString", discordImageKey, mock.Anything, mock.Anything).Return(nil)
httpReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(httpReq)
pdk.PDKMock.On("Send", mock.Anything).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`[{"external_asset_path":"external/art"}]`)))
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`[{"external_asset_path":"external/art"}]`)}, nil)
host.WebSocketMock.On("SendText", "testuser", mock.MatchedBy(func(msg string) bool {
return strings.Contains(msg, `"op":3`) &&
@@ -364,15 +354,9 @@ var _ = Describe("discordRPC", func() {
host.CacheMock.On("GetString", discordImageKey).Return("", false, nil)
host.CacheMock.On("SetString", discordImageKey, mock.Anything, mock.Anything).Return(nil)
trackReq := &pdk.HTTPRequest{}
defaultReq := &pdk.HTTPRequest{}
// First call (track art) returns 500, second call (default) succeeds
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(trackReq).Once()
pdk.PDKMock.On("Send", trackReq).Return(pdk.NewStubHTTPResponse(500, nil, []byte(`error`))).Once()
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(defaultReq).Once()
pdk.PDKMock.On("Send", defaultReq).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`[{"external_asset_path":"external/logo"}]`))).Once()
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 500, Body: []byte(`error`)}, nil).Once()
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`[{"external_asset_path":"external/logo"}]`)}, nil).Once()
host.WebSocketMock.On("SendText", "testuser", mock.MatchedBy(func(msg string) bool {
return strings.Contains(msg, `"op":3`) &&
@@ -400,9 +384,7 @@ var _ = Describe("discordRPC", func() {
It("clears all images when both track art and default fail", func() {
host.CacheMock.On("GetString", discordImageKey).Return("", false, nil)
httpReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(httpReq)
pdk.PDKMock.On("Send", mock.Anything).Return(pdk.NewStubHTTPResponse(200, nil, []byte(`{"not":"array"}`)))
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`{"not":"array"}`)}, nil)
host.WebSocketMock.On("SendText", "testuser", mock.MatchedBy(func(msg string) bool {
return strings.Contains(msg, `"op":3`) &&
@@ -431,9 +413,7 @@ var _ = Describe("discordRPC", func() {
host.CacheMock.On("GetString", discordImageKey).Return("mp:cached/large", true, nil).Once()
host.CacheMock.On("GetString", discordImageKey).Return("", false, nil).Once()
httpReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, externalAssetsURL).Return(httpReq)
pdk.PDKMock.On("Send", mock.Anything).Return(pdk.NewStubHTTPResponse(500, nil, []byte(`error`)))
host.HTTPMock.On("Send", externalAssetsReq).Return(&host.HTTPResponse{StatusCode: 500, Body: []byte(`error`)}, nil)
host.WebSocketMock.On("SendText", "testuser", mock.MatchedBy(func(msg string) bool {
return strings.Contains(msg, `"large_image":"mp:cached/large"`) &&
+28 -19
View File
@@ -49,19 +49,23 @@ func spotifyCacheKey(artist, title, album string) string {
// trySpotifyFromMBID calls the ListenBrainz spotify-id-from-mbid endpoint.
func trySpotifyFromMBID(mbid string) string {
body := fmt.Sprintf(`[{"recording_mbid":%q}]`, mbid)
req := pdk.NewHTTPRequest(pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-mbid/json")
req.SetHeader("Content-Type", "application/json")
req.SetBody([]byte(body))
resp := req.Send()
status := resp.Status()
if status < 200 || status >= 300 {
pdk.Log(pdk.LogDebug, fmt.Sprintf("ListenBrainz MBID lookup failed: HTTP %d, body=%s", status, string(resp.Body())))
resp, err := host.HTTPSend(host.HTTPRequest{
Method: "POST",
URL: "https://labs.api.listenbrainz.org/spotify-id-from-mbid/json",
Headers: map[string]string{"Content-Type": "application/json"},
Body: []byte(body),
})
if err != nil {
pdk.Log(pdk.LogInfo, fmt.Sprintf("ListenBrainz MBID lookup request failed: %v", err))
return ""
}
id := parseSpotifyID(resp.Body())
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
pdk.Log(pdk.LogDebug, fmt.Sprintf("ListenBrainz MBID lookup failed: HTTP %d, body=%s", resp.StatusCode, string(resp.Body)))
return ""
}
id := parseSpotifyID(resp.Body)
if id == "" {
pdk.Log(pdk.LogDebug, fmt.Sprintf("ListenBrainz MBID lookup returned no spotify_track_id for mbid=%s, body=%s", mbid, string(resp.Body())))
pdk.Log(pdk.LogDebug, fmt.Sprintf("ListenBrainz MBID lookup returned no spotify_track_id for mbid=%s, body=%s", mbid, string(resp.Body)))
}
return id
}
@@ -69,20 +73,25 @@ func trySpotifyFromMBID(mbid string) string {
// trySpotifyFromMetadata calls the ListenBrainz spotify-id-from-metadata endpoint.
func trySpotifyFromMetadata(artist, title, album string) string {
payload := fmt.Sprintf(`[{"artist_name":%q,"track_name":%q,"release_name":%q}]`, artist, title, album)
req := pdk.NewHTTPRequest(pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-metadata/json")
req.SetHeader("Content-Type", "application/json")
req.SetBody([]byte(payload))
pdk.Log(pdk.LogDebug, fmt.Sprintf("ListenBrainz metadata request: %s", payload))
resp := req.Send()
status := resp.Status()
if status < 200 || status >= 300 {
pdk.Log(pdk.LogDebug, fmt.Sprintf("ListenBrainz metadata lookup failed: HTTP %d, body=%s", status, string(resp.Body())))
resp, err := host.HTTPSend(host.HTTPRequest{
Method: "POST",
URL: "https://labs.api.listenbrainz.org/spotify-id-from-metadata/json",
Headers: map[string]string{"Content-Type": "application/json"},
Body: []byte(payload),
})
if err != nil {
pdk.Log(pdk.LogInfo, fmt.Sprintf("ListenBrainz metadata lookup request failed: %v", err))
return ""
}
pdk.Log(pdk.LogDebug, fmt.Sprintf("ListenBrainz metadata response: HTTP %d, body=%s", status, string(resp.Body())))
id := parseSpotifyID(resp.Body())
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
pdk.Log(pdk.LogDebug, fmt.Sprintf("ListenBrainz metadata lookup failed: HTTP %d, body=%s", resp.StatusCode, string(resp.Body)))
return ""
}
pdk.Log(pdk.LogDebug, fmt.Sprintf("ListenBrainz metadata response: HTTP %d, body=%s", resp.StatusCode, string(resp.Body)))
id := parseSpotifyID(resp.Body)
if id == "" {
pdk.Log(pdk.LogDebug, fmt.Sprintf("ListenBrainz metadata returned no spotify_track_id for %q - %q", artist, title))
}
+17 -18
View File
@@ -118,6 +118,8 @@ var _ = Describe("Spotify", func() {
pdk.ResetMock()
host.CacheMock.ExpectedCalls = nil
host.CacheMock.Calls = nil
host.HTTPMock.ExpectedCalls = nil
host.HTTPMock.Calls = nil
pdk.PDKMock.On("Log", mock.Anything, mock.Anything).Maybe()
})
@@ -138,10 +140,9 @@ var _ = Describe("Spotify", func() {
host.CacheMock.On("SetString", spotifyURLKey, mock.Anything, mock.Anything).Return(nil)
// Mock the MBID HTTP request
mbidReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-mbid/json").Return(mbidReq)
pdk.PDKMock.On("Send", mbidReq).Return(pdk.NewStubHTTPResponse(200, nil,
[]byte(`[{"spotify_track_ids":["63OQupATfueTdZMWIV7nzz"]}]`)))
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.URL == "https://labs.api.listenbrainz.org/spotify-id-from-mbid/json"
})).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`[{"spotify_track_ids":["63OQupATfueTdZMWIV7nzz"]}]`)}, nil)
url := resolveSpotifyURL(scrobbler.TrackInfo{
Title: "Karma Police",
@@ -159,15 +160,14 @@ var _ = Describe("Spotify", func() {
host.CacheMock.On("SetString", spotifyURLKey, mock.Anything, mock.Anything).Return(nil)
// MBID request fails
mbidReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-mbid/json").Return(mbidReq)
pdk.PDKMock.On("Send", mbidReq).Return(pdk.NewStubHTTPResponse(404, nil, []byte(`[]`)))
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.URL == "https://labs.api.listenbrainz.org/spotify-id-from-mbid/json"
})).Return(&host.HTTPResponse{StatusCode: 404, Body: []byte(`[]`)}, nil)
// Metadata request succeeds
metaReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-metadata/json").Return(metaReq)
pdk.PDKMock.On("Send", metaReq).Return(pdk.NewStubHTTPResponse(200, nil,
[]byte(`[{"spotify_track_ids":["4wlLbLeDWbA6TzwZFp1UaK"]}]`)))
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.URL == "https://labs.api.listenbrainz.org/spotify-id-from-metadata/json"
})).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`[{"spotify_track_ids":["4wlLbLeDWbA6TzwZFp1UaK"]}]`)}, nil)
url := resolveSpotifyURL(scrobbler.TrackInfo{
Title: "Karma Police",
@@ -184,9 +184,9 @@ var _ = Describe("Spotify", func() {
host.CacheMock.On("SetString", spotifyURLKey, mock.Anything, mock.Anything).Return(nil)
// No MBID, metadata request fails
metaReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-metadata/json").Return(metaReq)
pdk.PDKMock.On("Send", metaReq).Return(pdk.NewStubHTTPResponse(500, nil, []byte(`error`)))
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.URL == "https://labs.api.listenbrainz.org/spotify-id-from-metadata/json"
})).Return(&host.HTTPResponse{StatusCode: 500, Body: []byte(`error`)}, nil)
url := resolveSpotifyURL(scrobbler.TrackInfo{
Title: "Karma Police",
@@ -203,10 +203,9 @@ var _ = Describe("Spotify", func() {
host.CacheMock.On("GetString", spotifyURLKey).Return("", false, nil)
host.CacheMock.On("SetString", spotifyURLKey, mock.Anything, mock.Anything).Return(nil)
metaReq := &pdk.HTTPRequest{}
pdk.PDKMock.On("NewHTTPRequest", pdk.MethodPost, "https://labs.api.listenbrainz.org/spotify-id-from-metadata/json").Return(metaReq)
pdk.PDKMock.On("Send", metaReq).Return(pdk.NewStubHTTPResponse(200, nil,
[]byte(`[{"spotify_track_ids":["4tIGK5G9hNDA50ZdGioZRG"]}]`)))
host.HTTPMock.On("Send", mock.MatchedBy(func(req host.HTTPRequest) bool {
return req.URL == "https://labs.api.listenbrainz.org/spotify-id-from-metadata/json"
})).Return(&host.HTTPResponse{StatusCode: 200, Body: []byte(`[{"spotify_track_ids":["4tIGK5G9hNDA50ZdGioZRG"]}]`)}, nil)
url := resolveSpotifyURL(scrobbler.TrackInfo{
Title: "Some Song",