220 lines
9.0 KiB
Go
220 lines
9.0 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/navidrome/navidrome/plugins/pdk/go/host"
|
|
"github.com/navidrome/navidrome/plugins/pdk/go/pdk"
|
|
"github.com/navidrome/navidrome/plugins/pdk/go/scrobbler"
|
|
"github.com/stretchr/testify/mock"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
var _ = Describe("Spotify", func() {
|
|
Describe("spotifySearchURL", func() {
|
|
DescribeTable("constructs Spotify search URL",
|
|
func(expectedURL string, terms ...string) {
|
|
Expect(spotifySearchURL(terms...)).To(Equal(expectedURL))
|
|
},
|
|
Entry("artist and title", "https://open.spotify.com/search/Rick%20Astley%20Never%20Gonna%20Give%20You%20Up", "Rick Astley", "Never Gonna Give You Up"),
|
|
Entry("single term", "https://open.spotify.com/search/Radiohead", "Radiohead"),
|
|
Entry("empty terms", "", "", ""),
|
|
Entry("one empty term", "https://open.spotify.com/search/Solo%20Artist", "Solo Artist", ""),
|
|
)
|
|
})
|
|
|
|
Describe("spotifyCacheKey", func() {
|
|
It("produces identical keys for identical inputs", func() {
|
|
key1 := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
|
key2 := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
|
Expect(key1).To(Equal(key2))
|
|
})
|
|
|
|
It("produces different keys for different albums", func() {
|
|
key1 := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
|
key2 := spotifyCacheKey("Radiohead", "Karma Police", "The Bends")
|
|
Expect(key1).ToNot(Equal(key2))
|
|
})
|
|
|
|
It("uses the correct prefix", func() {
|
|
key := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
|
Expect(key).To(HavePrefix("spotify.url."))
|
|
})
|
|
|
|
It("is case-insensitive", func() {
|
|
keyUpper := spotifyCacheKey("Radiohead", "Karma Police", "OK Computer")
|
|
keyLower := spotifyCacheKey("radiohead", "karma police", "ok computer")
|
|
Expect(keyUpper).To(Equal(keyLower))
|
|
})
|
|
})
|
|
|
|
Describe("parseSpotifyID", func() {
|
|
DescribeTable("extracts first Spotify track ID from ListenBrainz response",
|
|
func(body, expectedID string) {
|
|
Expect(parseSpotifyID([]byte(body))).To(Equal(expectedID))
|
|
},
|
|
Entry("valid single result",
|
|
`[{"spotify_track_ids":["4tIGK5G9hNDA50ZdGioZRG"]}]`, "4tIGK5G9hNDA50ZdGioZRG"),
|
|
Entry("multiple IDs picks first",
|
|
`[{"artist_name":"Lil Baby & Drake","track_name":"Yes Indeed","spotify_track_ids":["6vN77lE9LK6HP2DewaN6HZ","4wlLbLeDWbA6TzwZFp1UaK"]}]`, "6vN77lE9LK6HP2DewaN6HZ"),
|
|
Entry("valid result with extra fields",
|
|
`[{"artist_name":"Radiohead","track_name":"Karma Police","spotify_track_ids":["63OQupATfueTdZMWIV7nzz"],"release_name":"OK Computer"}]`, "63OQupATfueTdZMWIV7nzz"),
|
|
Entry("empty spotify_track_ids array",
|
|
`[{"spotify_track_ids":[]}]`, ""),
|
|
Entry("no spotify_track_ids field",
|
|
`[{"artist_name":"Unknown"}]`, ""),
|
|
Entry("empty array",
|
|
`[]`, ""),
|
|
Entry("invalid JSON",
|
|
`not json`, ""),
|
|
Entry("null first result falls through to second",
|
|
`[{"spotify_track_ids":[]},{"spotify_track_ids":["6vN77lE9LK6HP2DewaN6HZ"]}]`, "6vN77lE9LK6HP2DewaN6HZ"),
|
|
Entry("skips invalid ID with special characters",
|
|
`[{"spotify_track_ids":["abc!@#$%^&*()_+=-12345"]}]`, ""),
|
|
)
|
|
})
|
|
|
|
Describe("isValidSpotifyID", func() {
|
|
DescribeTable("validates Spotify track IDs",
|
|
func(id string, expected bool) {
|
|
Expect(isValidSpotifyID(id)).To(Equal(expected))
|
|
},
|
|
Entry("valid 22-char ID", "6vN77lE9LK6HP2DewaN6HZ", true),
|
|
Entry("another valid ID", "4tIGK5G9hNDA50ZdGioZRG", true),
|
|
Entry("short valid ID", "abc123", true),
|
|
Entry("special characters", "6vN77lE9!K6HP2DewaN6HZ", false),
|
|
Entry("spaces", "6vN77 E9LK6HP2DewaN6HZ", false),
|
|
Entry("empty string", "", false),
|
|
)
|
|
})
|
|
|
|
Describe("ListenBrainz request payloads", func() {
|
|
It("builds valid JSON for MBID requests", func() {
|
|
mbid := "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
|
|
body := []byte(fmt.Sprintf(`[{"recording_mbid":%q}]`, mbid))
|
|
var parsed []map[string]string
|
|
Expect(json.Unmarshal(body, &parsed)).To(Succeed())
|
|
Expect(parsed[0]["recording_mbid"]).To(Equal(mbid))
|
|
})
|
|
|
|
It("builds valid JSON for metadata requests with special characters", func() {
|
|
artist := `Guns N' Roses`
|
|
title := `Sweet Child O' Mine`
|
|
album := `Appetite for Destruction`
|
|
payload := fmt.Sprintf(`[{"artist_name":%q,"track_name":%q,"release_name":%q}]`, artist, title, album)
|
|
var parsed []map[string]string
|
|
Expect(json.Unmarshal([]byte(payload), &parsed)).To(Succeed())
|
|
Expect(parsed[0]["artist_name"]).To(Equal(artist))
|
|
Expect(parsed[0]["track_name"]).To(Equal(title))
|
|
Expect(parsed[0]["release_name"]).To(Equal(album))
|
|
})
|
|
})
|
|
|
|
Describe("resolveSpotifyURL", func() {
|
|
BeforeEach(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()
|
|
})
|
|
|
|
It("returns cached URL on cache hit", func() {
|
|
host.CacheMock.On("GetString", spotifyURLKey).Return("https://open.spotify.com/track/cached123", true, nil)
|
|
|
|
url := resolveSpotifyURL(scrobbler.TrackInfo{
|
|
Title: "Karma Police",
|
|
Artist: "Radiohead",
|
|
Artists: []scrobbler.ArtistRef{{Name: "Radiohead"}},
|
|
Album: "OK Computer",
|
|
})
|
|
Expect(url).To(Equal("https://open.spotify.com/track/cached123"))
|
|
})
|
|
|
|
It("resolves via MBID when available", func() {
|
|
host.CacheMock.On("GetString", spotifyURLKey).Return("", false, nil)
|
|
host.CacheMock.On("SetString", spotifyURLKey, mock.Anything, mock.Anything).Return(nil)
|
|
|
|
// Mock the MBID HTTP request
|
|
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",
|
|
Artist: "Radiohead",
|
|
Artists: []scrobbler.ArtistRef{{Name: "Radiohead"}},
|
|
Album: "OK Computer",
|
|
MBZRecordingID: "mbid-123",
|
|
})
|
|
Expect(url).To(Equal("https://open.spotify.com/track/63OQupATfueTdZMWIV7nzz"))
|
|
host.CacheMock.AssertCalled(GinkgoT(), "SetString", spotifyURLKey, "https://open.spotify.com/track/63OQupATfueTdZMWIV7nzz", spotifyCacheTTLHit)
|
|
})
|
|
|
|
It("falls back to metadata lookup when MBID fails", func() {
|
|
host.CacheMock.On("GetString", spotifyURLKey).Return("", false, nil)
|
|
host.CacheMock.On("SetString", spotifyURLKey, mock.Anything, mock.Anything).Return(nil)
|
|
|
|
// MBID request fails
|
|
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
|
|
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",
|
|
Artist: "Radiohead",
|
|
Artists: []scrobbler.ArtistRef{{Name: "Radiohead"}},
|
|
Album: "OK Computer",
|
|
MBZRecordingID: "mbid-123",
|
|
})
|
|
Expect(url).To(Equal("https://open.spotify.com/track/4wlLbLeDWbA6TzwZFp1UaK"))
|
|
})
|
|
|
|
It("falls back to search URL when both lookups fail", func() {
|
|
host.CacheMock.On("GetString", spotifyURLKey).Return("", false, nil)
|
|
host.CacheMock.On("SetString", spotifyURLKey, mock.Anything, mock.Anything).Return(nil)
|
|
|
|
// No MBID, metadata request fails
|
|
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",
|
|
Artist: "Radiohead",
|
|
Artists: []scrobbler.ArtistRef{{Name: "Radiohead"}},
|
|
Album: "OK Computer",
|
|
})
|
|
Expect(url).To(HavePrefix("https://open.spotify.com/search/"))
|
|
Expect(url).To(ContainSubstring("Radiohead"))
|
|
host.CacheMock.AssertCalled(GinkgoT(), "SetString", spotifyURLKey, mock.Anything, spotifyCacheTTLMiss)
|
|
})
|
|
|
|
It("uses Artists[0] for primary artist", func() {
|
|
host.CacheMock.On("GetString", spotifyURLKey).Return("", false, nil)
|
|
host.CacheMock.On("SetString", spotifyURLKey, mock.Anything, mock.Anything).Return(nil)
|
|
|
|
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",
|
|
Artist: "",
|
|
Album: "Some Album",
|
|
Artists: []scrobbler.ArtistRef{{Name: "Fallback Artist"}},
|
|
})
|
|
Expect(url).To(Equal("https://open.spotify.com/track/4tIGK5G9hNDA50ZdGioZRG"))
|
|
})
|
|
})
|
|
})
|