mirror of
https://github.com/navidrome/navidrome.git
synced 2026-05-03 06:51:16 +00:00
Merge 5c5cae09705842e4edbebca8f0518e2d3e378346 into 13c48b38a0737236b79af02b4a7bd42cb6ee1b27
This commit is contained in:
commit
0d3820ad92
@ -64,7 +64,7 @@ func lastFMConstructor(ds model.DataStore) *lastfmAgent {
|
||||
}
|
||||
chc := cache.NewHTTPClient(hc, consts.DefaultHttpClientTimeOut)
|
||||
l.httpClient = chc
|
||||
l.client = newClient(l.apiKey, l.secret, chc)
|
||||
l.client = newClient(l.apiKey, l.secret, conf.Server.LastFM.BaseURL, chc)
|
||||
return l
|
||||
}
|
||||
|
||||
|
||||
@ -72,7 +72,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
var httpClient *tests.FakeHttpClient
|
||||
BeforeEach(func() {
|
||||
httpClient = &tests.FakeHttpClient{}
|
||||
client := newClient("API_KEY", "SECRET", httpClient)
|
||||
client := newClient("API_KEY", "SECRET", "", httpClient)
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = client
|
||||
})
|
||||
@ -114,7 +114,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
It("returns content in first language when available (1 API call)", func() {
|
||||
conf.Server.LastFM.Languages = []string{"pt", "en"}
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = newClient("API_KEY", "SECRET", httpClient)
|
||||
agent.client = newClient("API_KEY", "SECRET", "", httpClient)
|
||||
|
||||
// Portuguese biography available
|
||||
f, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.json")
|
||||
@ -131,7 +131,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
It("falls back to second language when first returns empty (2 API calls)", func() {
|
||||
conf.Server.LastFM.Languages = []string{"ja", "en"}
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = newClient("API_KEY", "SECRET", httpClient)
|
||||
agent.client = newClient("API_KEY", "SECRET", "", httpClient)
|
||||
|
||||
// Japanese returns empty/ignored biography (actual Last.fm response with just "Read more" link)
|
||||
fJa, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.empty.json")
|
||||
@ -152,7 +152,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
It("returns ErrNotFound when all languages return empty", func() {
|
||||
conf.Server.LastFM.Languages = []string{"ja", "xx"}
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = newClient("API_KEY", "SECRET", httpClient)
|
||||
agent.client = newClient("API_KEY", "SECRET", "", httpClient)
|
||||
|
||||
// Both languages return empty/ignored biography (using actual Last.fm response format)
|
||||
fJa, _ := os.Open("tests/fixtures/lastfm.artist.getinfo.empty.json")
|
||||
@ -179,7 +179,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
It("falls back to second language when first returns empty description (2 API calls)", func() {
|
||||
conf.Server.LastFM.Languages = []string{"ja", "en"}
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = newClient("API_KEY", "SECRET", httpClient)
|
||||
agent.client = newClient("API_KEY", "SECRET", "", httpClient)
|
||||
|
||||
// Japanese returns album without wiki/description (actual Last.fm response)
|
||||
fJa, _ := os.Open("tests/fixtures/lastfm.album.getinfo.empty.json")
|
||||
@ -201,7 +201,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
It("returns album without description when all languages return empty", func() {
|
||||
conf.Server.LastFM.Languages = []string{"ja", "xx"}
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = newClient("API_KEY", "SECRET", httpClient)
|
||||
agent.client = newClient("API_KEY", "SECRET", "", httpClient)
|
||||
|
||||
// Both languages return album without description
|
||||
fJa, _ := os.Open("tests/fixtures/lastfm.album.getinfo.empty.json")
|
||||
@ -224,7 +224,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
var httpClient *tests.FakeHttpClient
|
||||
BeforeEach(func() {
|
||||
httpClient = &tests.FakeHttpClient{}
|
||||
client := newClient("API_KEY", "SECRET", httpClient)
|
||||
client := newClient("API_KEY", "SECRET", "", httpClient)
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = client
|
||||
})
|
||||
@ -262,7 +262,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
var httpClient *tests.FakeHttpClient
|
||||
BeforeEach(func() {
|
||||
httpClient = &tests.FakeHttpClient{}
|
||||
client := newClient("API_KEY", "SECRET", httpClient)
|
||||
client := newClient("API_KEY", "SECRET", "", httpClient)
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = client
|
||||
})
|
||||
@ -300,7 +300,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
var httpClient *tests.FakeHttpClient
|
||||
BeforeEach(func() {
|
||||
httpClient = &tests.FakeHttpClient{}
|
||||
client := newClient("API_KEY", "SECRET", httpClient)
|
||||
client := newClient("API_KEY", "SECRET", "", httpClient)
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = client
|
||||
})
|
||||
@ -350,7 +350,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
BeforeEach(func() {
|
||||
_ = ds.UserProps(ctx).Put("user-1", sessionKeyProperty, "SK-1")
|
||||
httpClient = &tests.FakeHttpClient{}
|
||||
client := newClient("API_KEY", "SECRET", httpClient)
|
||||
client := newClient("API_KEY", "SECRET", "", httpClient)
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = client
|
||||
track = &model.MediaFile{
|
||||
@ -524,7 +524,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
var httpClient *tests.FakeHttpClient
|
||||
BeforeEach(func() {
|
||||
httpClient = &tests.FakeHttpClient{}
|
||||
client := newClient("API_KEY", "SECRET", httpClient)
|
||||
client := newClient("API_KEY", "SECRET", "", httpClient)
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = client
|
||||
})
|
||||
@ -594,7 +594,7 @@ var _ = Describe("lastfmAgent", func() {
|
||||
BeforeEach(func() {
|
||||
apiClient = &tests.FakeHttpClient{}
|
||||
httpClient = &tests.FakeHttpClient{}
|
||||
client := newClient("API_KEY", "SECRET", apiClient)
|
||||
client := newClient("API_KEY", "SECRET", "", apiClient)
|
||||
agent = lastFMConstructor(ds)
|
||||
agent.client = client
|
||||
agent.httpClient = httpClient
|
||||
|
||||
@ -31,6 +31,7 @@ type Router struct {
|
||||
client *client
|
||||
apiKey string
|
||||
secret string
|
||||
authURL string
|
||||
}
|
||||
|
||||
func NewRouter(ds model.DataStore) *Router {
|
||||
@ -38,13 +39,14 @@ func NewRouter(ds model.DataStore) *Router {
|
||||
ds: ds,
|
||||
apiKey: conf.Server.LastFM.ApiKey,
|
||||
secret: conf.Server.LastFM.Secret,
|
||||
authURL: conf.Server.LastFM.AuthURL,
|
||||
sessionKeys: &agents.SessionKeys{DataStore: ds, KeyName: sessionKeyProperty},
|
||||
}
|
||||
r.Handler = r.routes()
|
||||
hc := &http.Client{
|
||||
Timeout: consts.DefaultHttpClientTimeOut,
|
||||
}
|
||||
r.client = newClient(r.apiKey, r.secret, hc)
|
||||
r.client = newClient(r.apiKey, r.secret, conf.Server.LastFM.BaseURL, hc)
|
||||
return r
|
||||
}
|
||||
|
||||
@ -67,6 +69,7 @@ func (s *Router) routes() http.Handler {
|
||||
func (s *Router) getLinkStatus(w http.ResponseWriter, r *http.Request) {
|
||||
resp := map[string]any{
|
||||
"apiKey": s.apiKey,
|
||||
"authUrl": s.authURL,
|
||||
}
|
||||
u, _ := request.UserFrom(r.Context())
|
||||
key, err := s.sessionKeys.Get(r.Context(), u.ID)
|
||||
|
||||
@ -14,13 +14,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/log"
|
||||
)
|
||||
|
||||
const (
|
||||
apiBaseUrl = "https://ws.audioscrobbler.com/2.0/"
|
||||
)
|
||||
|
||||
type lastFMError struct {
|
||||
Code int
|
||||
Message string
|
||||
@ -34,13 +31,14 @@ type httpDoer interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
func newClient(apiKey string, secret string, hc httpDoer) *client {
|
||||
return &client{apiKey, secret, hc}
|
||||
func newClient(apiKey string, secret string, baseURL string, hc httpDoer) *client {
|
||||
return &client{apiKey, secret, normalizeLastFMBaseURL(baseURL), hc}
|
||||
}
|
||||
|
||||
type client struct {
|
||||
apiKey string
|
||||
secret string
|
||||
baseURL string
|
||||
hc httpDoer
|
||||
}
|
||||
|
||||
@ -200,10 +198,10 @@ func (c *client) makeRequest(ctx context.Context, method string, params url.Valu
|
||||
var req *http.Request
|
||||
if method == http.MethodPost {
|
||||
body := strings.NewReader(params.Encode())
|
||||
req, _ = http.NewRequestWithContext(ctx, method, apiBaseUrl, body)
|
||||
req, _ = http.NewRequestWithContext(ctx, method, c.baseURL, body)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
} else {
|
||||
req, _ = http.NewRequestWithContext(ctx, method, apiBaseUrl, nil)
|
||||
req, _ = http.NewRequestWithContext(ctx, method, c.baseURL, nil)
|
||||
req.URL.RawQuery = params.Encode()
|
||||
}
|
||||
|
||||
@ -231,6 +229,17 @@ func (c *client) makeRequest(ctx context.Context, method string, params url.Valu
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func normalizeLastFMBaseURL(baseURL string) string {
|
||||
baseURL = strings.TrimSpace(baseURL)
|
||||
if baseURL == "" {
|
||||
return consts.DefaultLastFMBaseURL
|
||||
}
|
||||
if !strings.HasSuffix(baseURL, "/") {
|
||||
return baseURL + "/"
|
||||
}
|
||||
return baseURL
|
||||
}
|
||||
|
||||
func (c *client) sign(params url.Values) {
|
||||
// the parameters must be in order before hashing
|
||||
keys := make([]string, 0, len(params))
|
||||
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/navidrome/navidrome/consts"
|
||||
"github.com/navidrome/navidrome/tests"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
@ -22,7 +23,7 @@ var _ = Describe("client", func() {
|
||||
|
||||
BeforeEach(func() {
|
||||
httpClient = &tests.FakeHttpClient{}
|
||||
client = newClient("API_KEY", "SECRET", httpClient)
|
||||
client = newClient("API_KEY", "SECRET", "", httpClient)
|
||||
})
|
||||
|
||||
Describe("albumGetInfo", func() {
|
||||
@ -33,7 +34,7 @@ var _ = Describe("client", func() {
|
||||
album, err := client.albumGetInfo(context.Background(), "Believe", "U2", "mbid-1234", "pt")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(album.Name).To(Equal("Believe"))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?album=Believe&api_key=API_KEY&artist=U2&format=json&lang=pt&mbid=mbid-1234&method=album.getInfo"))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(consts.DefaultLastFMBaseURL + "?album=Believe&api_key=API_KEY&artist=U2&format=json&lang=pt&mbid=mbid-1234&method=album.getInfo"))
|
||||
})
|
||||
})
|
||||
|
||||
@ -45,7 +46,7 @@ var _ = Describe("client", func() {
|
||||
artist, err := client.artistGetInfo(context.Background(), "U2", "pt")
|
||||
Expect(err).To(BeNil())
|
||||
Expect(artist.Name).To(Equal("U2"))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&lang=pt&method=artist.getInfo"))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(consts.DefaultLastFMBaseURL + "?api_key=API_KEY&artist=U2&format=json&lang=pt&method=artist.getInfo"))
|
||||
})
|
||||
|
||||
It("fails if Last.fm returns an http status != 200", func() {
|
||||
@ -105,7 +106,7 @@ var _ = Describe("client", func() {
|
||||
similar, err := client.artistGetSimilar(context.Background(), "U2", 2)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(len(similar.Artists)).To(Equal(2))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&limit=2&method=artist.getSimilar"))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(consts.DefaultLastFMBaseURL + "?api_key=API_KEY&artist=U2&format=json&limit=2&method=artist.getSimilar"))
|
||||
})
|
||||
})
|
||||
|
||||
@ -117,7 +118,7 @@ var _ = Describe("client", func() {
|
||||
top, err := client.artistGetTopTracks(context.Background(), "U2", 2)
|
||||
Expect(err).To(BeNil())
|
||||
Expect(len(top.Track)).To(Equal(2))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=U2&format=json&limit=2&method=artist.getTopTracks"))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(consts.DefaultLastFMBaseURL + "?api_key=API_KEY&artist=U2&format=json&limit=2&method=artist.getTopTracks"))
|
||||
})
|
||||
})
|
||||
|
||||
@ -132,7 +133,7 @@ var _ = Describe("client", func() {
|
||||
Expect(similar.Track[0].Name).To(Equal("Dreaming of Me"))
|
||||
Expect(similar.Track[0].Artist.Name).To(Equal("Depeche Mode"))
|
||||
Expect(similar.Track[0].Match).To(Equal(1.0))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(apiBaseUrl + "?api_key=API_KEY&artist=Depeche+Mode&format=json&limit=5&method=track.getSimilar&track=Just+Can%27t+Get+Enough"))
|
||||
Expect(httpClient.SavedRequest.URL.String()).To(Equal(consts.DefaultLastFMBaseURL + "?api_key=API_KEY&artist=Depeche+Mode&format=json&limit=5&method=track.getSimilar&track=Just+Can%27t+Get+Enough"))
|
||||
})
|
||||
|
||||
It("returns empty list when no similar tracks found", func() {
|
||||
|
||||
@ -185,6 +185,8 @@ type lastfmOptions struct {
|
||||
Enabled bool
|
||||
ApiKey string //nolint:gosec
|
||||
Secret string //nolint:gosec
|
||||
BaseURL string
|
||||
AuthURL string
|
||||
Language string
|
||||
ScrobbleFirstArtistOnly bool
|
||||
|
||||
@ -822,6 +824,8 @@ func setViperDefaults() {
|
||||
viper.SetDefault("lastfm.language", consts.DefaultInfoLanguage)
|
||||
viper.SetDefault("lastfm.apikey", "")
|
||||
viper.SetDefault("lastfm.secret", "")
|
||||
viper.SetDefault("lastfm.baseurl", consts.DefaultLastFMBaseURL)
|
||||
viper.SetDefault("lastfm.authurl", consts.DefaultLastFMAuthURL)
|
||||
viper.SetDefault("lastfm.scrobblefirstartistonly", false)
|
||||
viper.SetDefault("deezer.enabled", true)
|
||||
viper.SetDefault("deezer.language", consts.DefaultInfoLanguage)
|
||||
|
||||
@ -75,6 +75,8 @@ const (
|
||||
DefaultUIPlaybackReportInterval = time.Minute
|
||||
|
||||
DefaultHttpClientTimeOut = 10 * time.Second
|
||||
DefaultLastFMBaseURL = "https://ws.audioscrobbler.com/2.0/"
|
||||
DefaultLastFMAuthURL = "https://www.last.fm/api/auth/"
|
||||
|
||||
DefaultListenBrainzBaseURL = "https://api.listenbrainz.org/1/"
|
||||
DefaultListenBrainzArtistAlgorithm = "session_based_days_9000_session_300_contribution_5_threshold_15_limit_50_skip_30"
|
||||
|
||||
@ -12,8 +12,15 @@ import { useInterval } from '../common'
|
||||
import { baseUrl, openInNewTab } from '../utils'
|
||||
import { httpClient } from '../dataProvider'
|
||||
|
||||
const buildAuthUrl = (authUrl, apiKey, callbackUrl) => {
|
||||
const url = new URL(authUrl)
|
||||
url.searchParams.set('api_key', apiKey)
|
||||
url.searchParams.set('cb', callbackUrl)
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
const Progress = (props) => {
|
||||
const { setLinked, setCheckingLink, apiKey } = props
|
||||
const { setLinked, setCheckingLink, apiKey, authUrl } = props
|
||||
const notify = useNotify()
|
||||
let linkCheckDelay = 2000
|
||||
let linkChecks = 30
|
||||
@ -24,10 +31,16 @@ const Progress = (props) => {
|
||||
`/api/lastfm/link/callback?uid=${localStorage.getItem('userId')}`,
|
||||
)
|
||||
const callbackUrl = `${window.location.origin}${callbackEndpoint}`
|
||||
try {
|
||||
openedTab.current = openInNewTab(
|
||||
`https://www.last.fm/api/auth/?api_key=${apiKey}&cb=${callbackUrl}`,
|
||||
buildAuthUrl(authUrl, apiKey, callbackUrl),
|
||||
)
|
||||
}, [apiKey])
|
||||
} catch {
|
||||
setCheckingLink(false)
|
||||
setLinked(false)
|
||||
notify('message.lastfmLinkFailure', 'warning')
|
||||
}
|
||||
}, [apiKey, authUrl, notify, setCheckingLink, setLinked])
|
||||
|
||||
const endChecking = (success) => {
|
||||
linkCheckDelay = null
|
||||
@ -76,12 +89,14 @@ export const LastfmScrobbleToggle = (props) => {
|
||||
const [linked, setLinked] = useState(null)
|
||||
const [checkingLink, setCheckingLink] = useState(false)
|
||||
const [apiKey, setApiKey] = useState(false)
|
||||
const [authUrl, setAuthUrl] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
httpClient('/api/lastfm/link')
|
||||
.then((response) => {
|
||||
setLinked(response.json.status === true)
|
||||
setApiKey(response.json.apiKey)
|
||||
setAuthUrl(response.json.authUrl || null)
|
||||
})
|
||||
.catch(() => {
|
||||
setLinked(false)
|
||||
@ -90,6 +105,10 @@ export const LastfmScrobbleToggle = (props) => {
|
||||
|
||||
const toggleScrobble = () => {
|
||||
if (!linked) {
|
||||
if (!authUrl) {
|
||||
notify('message.lastfmLinkFailure', 'warning')
|
||||
return
|
||||
}
|
||||
setCheckingLink(true)
|
||||
} else {
|
||||
httpClient('/api/lastfm/link', { method: 'DELETE' })
|
||||
@ -109,7 +128,7 @@ export const LastfmScrobbleToggle = (props) => {
|
||||
id={'lastfm'}
|
||||
color="primary"
|
||||
checked={linked || checkingLink}
|
||||
disabled={!apiKey || linked === null || checkingLink}
|
||||
disabled={!apiKey || !authUrl || linked === null || checkingLink}
|
||||
onChange={toggleScrobble}
|
||||
/>
|
||||
}
|
||||
@ -122,6 +141,7 @@ export const LastfmScrobbleToggle = (props) => {
|
||||
setLinked={setLinked}
|
||||
setCheckingLink={setCheckingLink}
|
||||
apiKey={apiKey}
|
||||
authUrl={authUrl}
|
||||
/>
|
||||
)}
|
||||
{!apiKey && (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user