mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
fix tidal lyrics fetch host and synced lyric tagging
This commit is contained in:
@@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
baseURL = "https://api.tidalhifi.com/v1"
|
baseURL = "https://api.tidalhifi.com/v1"
|
||||||
|
lyricsAPIv1 = "https://api.tidal.com/v1"
|
||||||
openAPIV2 = "https://openapi.tidal.com/v2"
|
openAPIV2 = "https://openapi.tidal.com/v2"
|
||||||
authURL = "https://auth.tidal.com/v1/oauth2"
|
authURL = "https://auth.tidal.com/v1/oauth2"
|
||||||
clientID = "fX2JxdmntZWK0ixT"
|
clientID = "fX2JxdmntZWK0ixT"
|
||||||
@@ -54,6 +55,7 @@ type Client struct {
|
|||||||
http *http.Client
|
http *http.Client
|
||||||
limiter *ratelimit.Limiter
|
limiter *ratelimit.Limiter
|
||||||
baseURL string
|
baseURL string
|
||||||
|
lyricsAPI string
|
||||||
openAPI string
|
openAPI string
|
||||||
loggedIn bool
|
loggedIn bool
|
||||||
}
|
}
|
||||||
@@ -64,6 +66,7 @@ func New(cfg *config.Config) *Client {
|
|||||||
http: netutil.NewHTTPClient(30*time.Second, cfg.Session.Downloads.VerifySSL),
|
http: netutil.NewHTTPClient(30*time.Second, cfg.Session.Downloads.VerifySSL),
|
||||||
limiter: ratelimit.New(cfg.Session.Downloads.RequestsPerMinute),
|
limiter: ratelimit.New(cfg.Session.Downloads.RequestsPerMinute),
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
|
lyricsAPI: lyricsAPIv1,
|
||||||
openAPI: openAPIV2,
|
openAPI: openAPIV2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -206,11 +209,38 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
|
|||||||
if album, ok := resp["album"].(map[string]any); ok {
|
if album, ok := resp["album"].(map[string]any); ok {
|
||||||
enrichTidalImage(album)
|
enrichTidalImage(album)
|
||||||
}
|
}
|
||||||
|
if lyrics, lrc := c.fetchTrackLyrics(ctx, item); lyrics != "" || lrc != "" {
|
||||||
|
if lyrics != "" {
|
||||||
|
resp["lyrics"] = lyrics
|
||||||
|
}
|
||||||
|
if lrc != "" {
|
||||||
|
resp["lyrics_synced"] = lrc
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) fetchTrackLyrics(ctx context.Context, trackID string) (string, string) {
|
||||||
|
params := url.Values{}
|
||||||
|
params.Set("deviceType", "PHONE")
|
||||||
|
params.Set("locale", "en_US")
|
||||||
|
params.Set("platform", "ANDROID")
|
||||||
|
|
||||||
|
resp, status, err := c.apiRequest(ctx, "tracks/"+url.PathEscape(strings.TrimSpace(trackID))+"/lyrics", params, c.lyricsAPI)
|
||||||
|
if err != nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
if status != http.StatusOK {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
lyrics := strings.TrimSpace(stringify(resp["lyrics"]))
|
||||||
|
lrc := strings.TrimSpace(stringify(resp["subtitles"]))
|
||||||
|
return lyrics, lrc
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) Search(ctx context.Context, mediaType, query string, limit int) ([]map[string]any, error) {
|
func (c *Client) Search(ctx context.Context, mediaType, query string, limit int) ([]map[string]any, error) {
|
||||||
if !c.loggedIn {
|
if !c.loggedIn {
|
||||||
return nil, errors.New("tidal client not logged in")
|
return nil, errors.New("tidal client not logged in")
|
||||||
|
|||||||
@@ -162,6 +162,82 @@ func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetMetadataTrackAddsLyricsAndSyncedLyrics(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/v1/tracks/42":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "Song", "album": map[string]any{"id": 10, "title": "Album"}})
|
||||||
|
case "/v1/tracks/42/lyrics":
|
||||||
|
q := r.URL.Query()
|
||||||
|
if q.Get("deviceType") != "PHONE" || q.Get("locale") != "en_US" || q.Get("platform") != "ANDROID" || q.Get("countryCode") != "MY" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": "bad query"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"lyrics": "plain lyrics line",
|
||||||
|
"subtitles": "[00:00.00]plain lyrics line",
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
cfgData := config.DefaultConfigData()
|
||||||
|
cfgData.Tidal.AccessToken = "token"
|
||||||
|
cfgData.Tidal.CountryCode = "MY"
|
||||||
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||||
|
c.loggedIn = true
|
||||||
|
c.baseURL = ts.URL + "/v1"
|
||||||
|
c.lyricsAPI = ts.URL + "/v1"
|
||||||
|
|
||||||
|
meta, err := c.GetMetadata(context.Background(), "42", "track")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetMetadata() err = %v", err)
|
||||||
|
}
|
||||||
|
if got := stringify(meta["lyrics"]); got != "plain lyrics line" {
|
||||||
|
t.Fatalf("lyrics = %q, want plain lyrics line", got)
|
||||||
|
}
|
||||||
|
if got := stringify(meta["lyrics_synced"]); got != "[00:00.00]plain lyrics line" {
|
||||||
|
t.Fatalf("lyrics_synced = %q, want synced lrc", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetMetadataTrackIgnoresLyricsEndpointFailure(t *testing.T) {
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/v1/tracks/42":
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "Song"})
|
||||||
|
case "/v1/tracks/42/lyrics":
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": "not found"})
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
cfgData := config.DefaultConfigData()
|
||||||
|
cfgData.Tidal.AccessToken = "token"
|
||||||
|
cfgData.Tidal.CountryCode = "US"
|
||||||
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||||
|
c.loggedIn = true
|
||||||
|
c.baseURL = ts.URL + "/v1"
|
||||||
|
c.lyricsAPI = ts.URL + "/v1"
|
||||||
|
|
||||||
|
meta, err := c.GetMetadata(context.Background(), "42", "track")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetMetadata() err = %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := meta["lyrics"]; ok {
|
||||||
|
t.Fatalf("did not expect lyrics when endpoint fails")
|
||||||
|
}
|
||||||
|
if _, ok := meta["lyrics_synced"]; ok {
|
||||||
|
t.Fatalf("did not expect lyrics_synced when endpoint fails")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetDownloadablePrefersAtmosWhenEnabled(t *testing.T) {
|
func TestGetDownloadablePrefersAtmosWhenEnabled(t *testing.T) {
|
||||||
var calls []string
|
var calls []string
|
||||||
allImmersive := true
|
allImmersive := true
|
||||||
|
|||||||
Reference in New Issue
Block a user