mirror of
https://github.com/navidrome/navidrome.git
synced 2026-08-01 07:21:17 +00:00
add similar artists from labs
This commit is contained in:
parent
ffd9167d3d
commit
68d36bdcec
@ -159,6 +159,31 @@ func (l *listenBrainzAgent) GetArtistTopSongs(ctx context.Context, id, artistNam
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (l *listenBrainzAgent) GetSimilarArtists(ctx context.Context, id string, name string, mbid string, limit int) ([]agents.Artist, error) {
|
||||
if mbid == "" {
|
||||
return nil, agents.ErrNotFound
|
||||
}
|
||||
|
||||
resp, err := l.client.getSimilarArtists(ctx, mbid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp) == 0 {
|
||||
return nil, agents.ErrNotFound
|
||||
}
|
||||
|
||||
artistCount := min(len(resp), limit)
|
||||
artists := make([]agents.Artist, artistCount)
|
||||
|
||||
for i := 0; i < artistCount; i++ {
|
||||
artists[i].MBID = resp[i].MBID
|
||||
artists[i].Name = resp[i].Name
|
||||
}
|
||||
|
||||
return artists, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
conf.AddHook(func() {
|
||||
if conf.Server.ListenBrainz.Enabled {
|
||||
@ -172,21 +197,21 @@ func init() {
|
||||
return nil
|
||||
})
|
||||
|
||||
if conf.Server.ListenBrainz.EnableMetadata {
|
||||
if conf.Server.ListenBrainz.BaseURL != "https://api.listenbrainz.org/1/" {
|
||||
log.Warn("ListenBrainz is enabled with a nonstandard API endpoint. Requests will likely not work")
|
||||
agents.Register(listenBrainzAgentName, func(ds model.DataStore) agents.Interface {
|
||||
// This is a workaround for the fact that a (Interface)(nil) is not the same as a (*listenBrainzConstructor)(nil)
|
||||
// See https://go.dev/doc/faq#nil_error
|
||||
a := listenBrainzConstructor(ds)
|
||||
if a != nil {
|
||||
return a
|
||||
}
|
||||
|
||||
agents.Register(listenBrainzAgentName, func(ds model.DataStore) agents.Interface {
|
||||
// This is a workaround for the fact that a (Interface)(nil) is not the same as a (*listenBrainzConstructor)(nil)
|
||||
// See https://go.dev/doc/faq#nil_error
|
||||
a := listenBrainzConstructor(ds)
|
||||
if a != nil {
|
||||
return a
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
_ agents.ArtistTopSongsRetriever = (*listenBrainzAgent)(nil)
|
||||
_ agents.ArtistURLRetriever = (*listenBrainzAgent)(nil)
|
||||
_ agents.ArtistSimilarRetriever = (*listenBrainzAgent)(nil)
|
||||
)
|
||||
|
||||
@ -228,7 +228,7 @@ var _ = Describe("listenBrainzAgent", func() {
|
||||
_, err := agent.GetArtistTopSongs(ctx, "", "", "d2a92ee2-27ce-4e71-bfc5-12e34fe8ef56", 1)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(httpClient.RequestCount).To(Equal(1))
|
||||
Expect(httpClient.SavedRequest.URL.Path).To(Equal("BASE_URL/popularity/top-recordings-for-artist/d2a92ee2-27ce-4e71-bfc5-12e34fe8ef56"))
|
||||
Expect(httpClient.SavedRequest.URL.Path).To(Equal("/1/popularity/top-recordings-for-artist/d2a92ee2-27ce-4e71-bfc5-12e34fe8ef56"))
|
||||
})
|
||||
|
||||
It("returns an error on listenbrainz error", func() {
|
||||
@ -239,7 +239,7 @@ var _ = Describe("listenBrainzAgent", func() {
|
||||
_, err := agent.GetArtistTopSongs(ctx, "", "", "1", 1)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(httpClient.RequestCount).To(Equal(1))
|
||||
Expect(httpClient.SavedRequest.URL.Path).To(Equal("BASE_URL/popularity/top-recordings-for-artist/1"))
|
||||
Expect(httpClient.SavedRequest.URL.Path).To(Equal("/1/popularity/top-recordings-for-artist/1"))
|
||||
})
|
||||
|
||||
It("returns all tracks when asked", func() {
|
||||
@ -290,4 +290,63 @@ var _ = Describe("listenBrainzAgent", func() {
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("GetSimilarArtists", func() {
|
||||
var agent *listenBrainzAgent
|
||||
var httpClient *tests.FakeHttpClient
|
||||
baseUrl := "https://labs.api.listenbrainz.org/similar-artists/json"
|
||||
|
||||
BeforeEach(func() {
|
||||
httpClient = &tests.FakeHttpClient{}
|
||||
client := newClient("BASE_URL", httpClient)
|
||||
agent = listenBrainzConstructor(ds)
|
||||
agent.client = client
|
||||
})
|
||||
|
||||
It("returns error when fetch calls", func() {
|
||||
httpClient.Err = errors.New("error")
|
||||
_, err := agent.GetSimilarArtists(ctx, "", "", "db92a151-1ac2-438b-bc43-b82e149ddd50", 1)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(httpClient.RequestCount).To(Equal(1))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(baseUrl + "?algorithm=session_based_days_9000_session_300_contribution_5_threshold_15_limit_50_skip_30&artist_mbids=db92a151-1ac2-438b-bc43-b82e149ddd50"))
|
||||
})
|
||||
|
||||
It("returns an error on listenbrainz error", func() {
|
||||
httpClient.Res = http.Response{
|
||||
Body: io.NopCloser(bytes.NewBufferString(`Bad request`)),
|
||||
StatusCode: 400,
|
||||
}
|
||||
_, err := agent.GetSimilarArtists(ctx, "", "", "1", 1)
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(httpClient.RequestCount).To(Equal(1))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(baseUrl + "?algorithm=session_based_days_9000_session_300_contribution_5_threshold_15_limit_50_skip_30&artist_mbids=1"))
|
||||
})
|
||||
|
||||
It("returns all data on call", func() {
|
||||
f, _ := os.Open("tests/fixtures/listenbrainz.labs.similar-artists.json")
|
||||
httpClient.Res = http.Response{Body: f, StatusCode: 200}
|
||||
|
||||
resp, err := agent.GetSimilarArtists(ctx, "", "", "db92a151-1ac2-438b-bc43-b82e149ddd50", 2)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(httpClient.RequestCount).To(Equal(1))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(baseUrl + "?algorithm=session_based_days_9000_session_300_contribution_5_threshold_15_limit_50_skip_30&artist_mbids=db92a151-1ac2-438b-bc43-b82e149ddd50"))
|
||||
Expect(resp).To(Equal([]agents.Artist{
|
||||
{MBID: "f27ec8db-af05-4f36-916e-3d57f91ecf5e", Name: "Michael Jackson"},
|
||||
{MBID: "7364dea6-ca9a-48e3-be01-b44ad0d19897", Name: "a-ha"},
|
||||
}))
|
||||
})
|
||||
|
||||
It("returns subset of data on call", func() {
|
||||
f, _ := os.Open("tests/fixtures/listenbrainz.labs.similar-artists.json")
|
||||
httpClient.Res = http.Response{Body: f, StatusCode: 200}
|
||||
|
||||
resp, err := agent.GetSimilarArtists(ctx, "", "", "db92a151-1ac2-438b-bc43-b82e149ddd50", 1)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(httpClient.RequestCount).To(Equal(1))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(baseUrl + "?algorithm=session_based_days_9000_session_300_contribution_5_threshold_15_limit_50_skip_30&artist_mbids=db92a151-1ac2-438b-bc43-b82e149ddd50"))
|
||||
Expect(resp).To(Equal([]agents.Artist{
|
||||
{MBID: "f27ec8db-af05-4f36-916e-3d57f91ecf5e", Name: "Michael Jackson"},
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -13,6 +13,13 @@ import (
|
||||
"github.com/navidrome/navidrome/log"
|
||||
)
|
||||
|
||||
const (
|
||||
lbzApiUrl = "https://api.listenbrainz.org/1/"
|
||||
labsBase = "https://labs.api.listenbrainz.org/"
|
||||
// There are a couple of algorithms from https://labs.api.listenbrainz.org/similar-artists
|
||||
algorithm = "session_based_days_9000_session_300_contribution_5_threshold_15_limit_50_skip_30"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrorNotFound = errors.New("listenbrainz: not found")
|
||||
)
|
||||
@ -189,11 +196,7 @@ type lbzHttpError struct {
|
||||
}
|
||||
|
||||
func (c *client) makeGenericRequest(ctx context.Context, method string, endpoint string, params url.Values) (*http.Response, error) {
|
||||
uri, err := c.path(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, _ := http.NewRequestWithContext(ctx, method, uri, nil)
|
||||
req, _ := http.NewRequestWithContext(ctx, method, lbzApiUrl+endpoint, nil)
|
||||
req.Header.Add("Content-Type", "application/json; charset=UTF-8")
|
||||
req.URL.RawQuery = params.Encode()
|
||||
|
||||
@ -283,3 +286,34 @@ func (c *client) getArtistTopSongs(ctx context.Context, mbid string, count int)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
type artist struct {
|
||||
MBID string `json:"artist_mbid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (c *client) getSimilarArtists(ctx context.Context, mbid string) ([]artist, error) {
|
||||
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, labsBase+"similar-artists/json", nil)
|
||||
req.Header.Add("Content-Type", "application/json; charset=UTF-8")
|
||||
req.URL.RawQuery = url.Values{
|
||||
"artist_mbids": []string{mbid}, "algorithm": []string{algorithm},
|
||||
}.Encode()
|
||||
|
||||
log.Trace(ctx, fmt.Sprintf("Sending ListenBrainz Labs %s request", req.Method), "url", req.URL)
|
||||
resp, err := c.hc.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
|
||||
var artists []artist
|
||||
jsonErr := decoder.Decode(&artists)
|
||||
if jsonErr != nil {
|
||||
return nil, fmt.Errorf("ListenBrainz: HTTP Error, Status: (%d)", resp.StatusCode)
|
||||
}
|
||||
|
||||
return artists, nil
|
||||
}
|
||||
|
||||
@ -119,7 +119,7 @@ var _ = Describe("client", func() {
|
||||
})
|
||||
|
||||
Context("getArtistUrl", func() {
|
||||
baseUrl := "BASE_URL/metadata/artist?"
|
||||
baseUrl := "https://api.listenbrainz.org/1/metadata/artist?"
|
||||
It("handles a malformed request with status code", func() {
|
||||
httpClient.Res = http.Response{
|
||||
Body: io.NopCloser(bytes.NewBufferString(`{"code": 400,"error": "artist mbid 1 is not valid."}`)),
|
||||
@ -167,7 +167,7 @@ var _ = Describe("client", func() {
|
||||
})
|
||||
|
||||
Context("getArtistTopSongs", func() {
|
||||
baseUrl := "BASE_URL/popularity/top-recordings-for-artist/"
|
||||
baseUrl := "https://api.listenbrainz.org/1/popularity/top-recordings-for-artist/"
|
||||
|
||||
It("handles a malformed request with status code", func() {
|
||||
httpClient.Res = http.Response{
|
||||
@ -244,4 +244,34 @@ var _ = Describe("client", func() {
|
||||
Expect(httpClient.SavedRequest.Header.Get("Content-Type")).To(Equal("application/json; charset=UTF-8"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("getSimilarArtists", func() {
|
||||
baseUrl := "https://labs.api.listenbrainz.org/similar-artists/json"
|
||||
|
||||
It("handles a malformed request with status code", func() {
|
||||
httpClient.Res = http.Response{
|
||||
Body: io.NopCloser(bytes.NewBufferString(`Bad request`)),
|
||||
StatusCode: 400,
|
||||
}
|
||||
_, err := client.getSimilarArtists(context.Background(), "1")
|
||||
Expect(err.Error()).To(Equal("ListenBrainz: HTTP Error, Status: (400)"))
|
||||
Expect(httpClient.SavedRequest.Method).To(Equal(http.MethodGet))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(baseUrl + "?algorithm=session_based_days_9000_session_300_contribution_5_threshold_15_limit_50_skip_30&artist_mbids=1"))
|
||||
Expect(httpClient.SavedRequest.Header.Get("Content-Type")).To(Equal("application/json; charset=UTF-8"))
|
||||
})
|
||||
|
||||
It("handles real data properly", func() {
|
||||
f, _ := os.Open("tests/fixtures/listenbrainz.labs.similar-artists.json")
|
||||
httpClient.Res = http.Response{Body: f, StatusCode: 200}
|
||||
resp, err := client.getSimilarArtists(context.Background(), "db92a151-1ac2-438b-bc43-b82e149ddd50")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(httpClient.SavedRequest.Method).To(Equal(http.MethodGet))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(baseUrl + "?algorithm=session_based_days_9000_session_300_contribution_5_threshold_15_limit_50_skip_30&artist_mbids=db92a151-1ac2-438b-bc43-b82e149ddd50"))
|
||||
Expect(httpClient.SavedRequest.Header.Get("Content-Type")).To(Equal("application/json; charset=UTF-8"))
|
||||
Expect(resp).To(Equal([]artist{
|
||||
{MBID: "f27ec8db-af05-4f36-916e-3d57f91ecf5e", Name: "Michael Jackson"},
|
||||
{MBID: "7364dea6-ca9a-48e3-be01-b44ad0d19897", Name: "a-ha"},
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -194,9 +194,8 @@ type deezerOptions struct {
|
||||
}
|
||||
|
||||
type listenBrainzOptions struct {
|
||||
Enabled bool
|
||||
EnableMetadata bool
|
||||
BaseURL string
|
||||
Enabled bool
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
type httpHeaderOptions struct {
|
||||
@ -657,7 +656,6 @@ func setViperDefaults() {
|
||||
viper.SetDefault("deezer.enabled", true)
|
||||
viper.SetDefault("deezer.language", consts.DefaultInfoLanguage)
|
||||
viper.SetDefault("listenbrainz.enabled", true)
|
||||
viper.SetDefault("listenbrainz.enablemetadata", true)
|
||||
viper.SetDefault("listenbrainz.baseurl", "https://api.listenbrainz.org/1/")
|
||||
viper.SetDefault("enablescrobblehistory", true)
|
||||
viper.SetDefault("httpheaders.frameoptions", "DENY")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user