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:
@@ -22,11 +22,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
baseURL = "https://api.tidalhifi.com/v1"
|
||||
openAPIV2 = "https://openapi.tidal.com/v2"
|
||||
authURL = "https://auth.tidal.com/v1/oauth2"
|
||||
clientID = "fX2JxdmntZWK0ixT"
|
||||
clientSec = "1Nm5AfDAjxrgJFJbKNWLeAyKGVGmINuXPPLHVXAvxAg="
|
||||
baseURL = "https://api.tidalhifi.com/v1"
|
||||
lyricsAPIv1 = "https://api.tidal.com/v1"
|
||||
openAPIV2 = "https://openapi.tidal.com/v2"
|
||||
authURL = "https://auth.tidal.com/v1/oauth2"
|
||||
clientID = "fX2JxdmntZWK0ixT"
|
||||
clientSec = "1Nm5AfDAjxrgJFJbKNWLeAyKGVGmINuXPPLHVXAvxAg="
|
||||
)
|
||||
|
||||
var qualityMap = map[int]string{
|
||||
@@ -50,21 +51,23 @@ var atmosAudioQualities = []string{"HI_RES_LOSSLESS", "HI_RES", "LOSSLESS", "HIG
|
||||
var ErrMissingTidalToken = errors.New("missing tidal access_token")
|
||||
|
||||
type Client struct {
|
||||
cfg *config.Config
|
||||
http *http.Client
|
||||
limiter *ratelimit.Limiter
|
||||
baseURL string
|
||||
openAPI string
|
||||
loggedIn bool
|
||||
cfg *config.Config
|
||||
http *http.Client
|
||||
limiter *ratelimit.Limiter
|
||||
baseURL string
|
||||
lyricsAPI string
|
||||
openAPI string
|
||||
loggedIn bool
|
||||
}
|
||||
|
||||
func New(cfg *config.Config) *Client {
|
||||
return &Client{
|
||||
cfg: cfg,
|
||||
http: netutil.NewHTTPClient(30*time.Second, cfg.Session.Downloads.VerifySSL),
|
||||
limiter: ratelimit.New(cfg.Session.Downloads.RequestsPerMinute),
|
||||
baseURL: baseURL,
|
||||
openAPI: openAPIV2,
|
||||
cfg: cfg,
|
||||
http: netutil.NewHTTPClient(30*time.Second, cfg.Session.Downloads.VerifySSL),
|
||||
limiter: ratelimit.New(cfg.Session.Downloads.RequestsPerMinute),
|
||||
baseURL: baseURL,
|
||||
lyricsAPI: lyricsAPIv1,
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
if !c.loggedIn {
|
||||
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) {
|
||||
var calls []string
|
||||
allImmersive := true
|
||||
|
||||
Reference in New Issue
Block a user