mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
fix tidal lossless quality negotiation and atmos format fallback
This commit is contained in:
@@ -37,12 +37,12 @@ var qualityMap = map[int]string{
|
|||||||
4: "HI_RES_LOSSLESS",
|
4: "HI_RES_LOSSLESS",
|
||||||
}
|
}
|
||||||
|
|
||||||
var qualityToFormat = map[int]string{
|
var qualityToFormats = map[int][]string{
|
||||||
0: "HEAACV1",
|
0: {"HEAACV1"},
|
||||||
1: "AACLC",
|
1: {"HEAACV1", "AACLC"},
|
||||||
2: "FLAC",
|
2: {"HEAACV1", "AACLC", "FLAC"},
|
||||||
3: "FLAC_HIRES",
|
3: {"HEAACV1", "AACLC", "FLAC", "FLAC_HIRES"},
|
||||||
4: "FLAC_HIRES",
|
4: {"HEAACV1", "AACLC", "FLAC", "FLAC_HIRES"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var atmosAudioQualities = []string{"HI_RES_LOSSLESS", "HI_RES", "LOSSLESS", "HIGH"}
|
var atmosAudioQualities = []string{"HI_RES_LOSSLESS", "HI_RES", "LOSSLESS", "HIGH"}
|
||||||
@@ -54,6 +54,7 @@ type Client struct {
|
|||||||
http *http.Client
|
http *http.Client
|
||||||
limiter *ratelimit.Limiter
|
limiter *ratelimit.Limiter
|
||||||
baseURL string
|
baseURL string
|
||||||
|
openAPI string
|
||||||
loggedIn bool
|
loggedIn bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,6 +64,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,
|
||||||
|
openAPI: openAPIV2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +265,11 @@ func (c *Client) GetDownloadable(ctx context.Context, trackID string, quality in
|
|||||||
}
|
}
|
||||||
if status == http.StatusOK {
|
if status == http.StatusOK {
|
||||||
if d := downloadableFromPlaybackManifest(resp); d != nil {
|
if d := downloadableFromPlaybackManifest(resp); d != nil {
|
||||||
|
if quality >= 2 && d.Extension == "m4a" {
|
||||||
|
if strict, strictErr := c.getDownloadableFromTrackManifest(ctx, trackID, quality); strictErr == nil && strict != nil {
|
||||||
|
return strict, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -475,19 +482,23 @@ func (c *Client) fetchArtistAlbums(ctx context.Context, artistID string) ([]map[
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getDownloadableFromTrackManifest(ctx context.Context, trackID string, quality int) (*provider.Downloadable, error) {
|
func (c *Client) getDownloadableFromTrackManifest(ctx context.Context, trackID string, quality int) (*provider.Downloadable, error) {
|
||||||
format := qualityToFormat[quality]
|
formats := formatsForQuality(quality, c.cfg.Session.Tidal.PreferAtmos)
|
||||||
return c.getDownloadableFromTrackManifestForFormat(ctx, trackID, format)
|
return c.getDownloadableFromTrackManifestForFormats(ctx, trackID, formats)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) getDownloadableFromTrackManifestForFormat(ctx context.Context, trackID, format string) (*provider.Downloadable, error) {
|
func (c *Client) getDownloadableFromTrackManifestForFormat(ctx context.Context, trackID, format string) (*provider.Downloadable, error) {
|
||||||
|
return c.getDownloadableFromTrackManifestForFormats(ctx, trackID, []string{format})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getDownloadableFromTrackManifestForFormats(ctx context.Context, trackID string, formats []string) (*provider.Downloadable, error) {
|
||||||
params := url.Values{}
|
params := url.Values{}
|
||||||
params.Set("manifestType", "MPEG_DASH")
|
params.Set("manifestType", "MPEG_DASH")
|
||||||
params.Set("formats", format)
|
params.Set("formats", strings.Join(formats, ","))
|
||||||
params.Set("uriScheme", "HTTPS")
|
params.Set("uriScheme", "HTTPS")
|
||||||
params.Set("usage", "PLAYBACK")
|
params.Set("usage", "PLAYBACK")
|
||||||
params.Set("adaptive", "false")
|
params.Set("adaptive", "false")
|
||||||
|
|
||||||
resp, status, err := c.apiRequest(ctx, "trackManifests/"+trackID, params, openAPIV2)
|
resp, status, err := c.apiRequest(ctx, "trackManifests/"+trackID, params, c.openAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -507,9 +518,9 @@ func (c *Client) getDownloadableFromTrackManifestForFormat(ctx context.Context,
|
|||||||
if uri == "" {
|
if uri == "" {
|
||||||
return nil, errors.New("tidal trackManifests missing uri")
|
return nil, errors.New("tidal trackManifests missing uri")
|
||||||
}
|
}
|
||||||
formats, _ := attrs["formats"].([]any)
|
attrFormats, _ := attrs["formats"].([]any)
|
||||||
ext := "m4a"
|
ext := "m4a"
|
||||||
for _, f := range formats {
|
for _, f := range attrFormats {
|
||||||
fv := strings.ToUpper(stringify(f))
|
fv := strings.ToUpper(stringify(f))
|
||||||
if strings.Contains(fv, "FLAC") {
|
if strings.Contains(fv, "FLAC") {
|
||||||
ext = "flac"
|
ext = "flac"
|
||||||
@@ -523,6 +534,18 @@ func (c *Client) getDownloadableFromTrackManifestForFormat(ctx context.Context,
|
|||||||
return &provider.Downloadable{URL: uri, Extension: ext, Source: "tidal"}, nil
|
return &provider.Downloadable{URL: uri, Extension: ext, Source: "tidal"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatsForQuality(quality int, preferAtmos bool) []string {
|
||||||
|
base, ok := qualityToFormats[quality]
|
||||||
|
if !ok {
|
||||||
|
base = qualityToFormats[0]
|
||||||
|
}
|
||||||
|
out := append([]string(nil), base...)
|
||||||
|
if preferAtmos {
|
||||||
|
out = append(out, "EAC3_JOC")
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) GetVideoDownloadable(ctx context.Context, videoID string) (*provider.Downloadable, error) {
|
func (c *Client) GetVideoDownloadable(ctx context.Context, videoID string) (*provider.Downloadable, 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")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -243,3 +244,75 @@ func TestTrackSupportsAtmosFromTags(t *testing.T) {
|
|||||||
t.Fatalf("expected atmos support from mediaMetadata tags")
|
t.Fatalf("expected atmos support from mediaMetadata tags")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFormatsForQuality(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
q int
|
||||||
|
atmos bool
|
||||||
|
wants []string
|
||||||
|
}{
|
||||||
|
{name: "low", q: 0, wants: []string{"HEAACV1"}},
|
||||||
|
{name: "high", q: 1, wants: []string{"HEAACV1", "AACLC"}},
|
||||||
|
{name: "lossless", q: 2, wants: []string{"HEAACV1", "AACLC", "FLAC"}},
|
||||||
|
{name: "hires", q: 4, wants: []string{"HEAACV1", "AACLC", "FLAC", "FLAC_HIRES"}},
|
||||||
|
{name: "atmos adds eac3", q: 2, atmos: true, wants: []string{"HEAACV1", "AACLC", "FLAC", "EAC3_JOC"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := formatsForQuality(tc.q, tc.atmos)
|
||||||
|
if !reflect.DeepEqual(got, tc.wants) {
|
||||||
|
t.Fatalf("formatsForQuality(%d, atmos=%v) = %#v, want %#v", tc.q, tc.atmos, got, tc.wants)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetDownloadableLosslessUsesTrackManifestWhenPlaybackIsAAC(t *testing.T) {
|
||||||
|
var gotFormats string
|
||||||
|
var ts *httptest.Server
|
||||||
|
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/v1/tracks/42/playbackinfopostpaywall":
|
||||||
|
manifest := map[string]any{"urls": []string{ts.URL + "/aac.m3u8"}, "codecs": "mp4a.40.2"}
|
||||||
|
b, _ := json.Marshal(manifest)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"manifest": base64.StdEncoding.EncodeToString(b)})
|
||||||
|
case "/v2/trackManifests/42":
|
||||||
|
gotFormats = r.URL.Query().Get("formats")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"data": map[string]any{
|
||||||
|
"attributes": map[string]any{
|
||||||
|
"uri": ts.URL + "/song.flac",
|
||||||
|
"formats": []any{"FLAC"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
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.openAPI = ts.URL + "/v2"
|
||||||
|
|
||||||
|
d, err := c.GetDownloadable(context.Background(), "42", 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetDownloadable() err = %v", err)
|
||||||
|
}
|
||||||
|
if gotFormats != "HEAACV1,AACLC,FLAC" {
|
||||||
|
t.Fatalf("formats query = %q, want %q", gotFormats, "HEAACV1,AACLC,FLAC")
|
||||||
|
}
|
||||||
|
if d.URL != ts.URL+"/song.flac" {
|
||||||
|
t.Fatalf("url = %q, want %q", d.URL, ts.URL+"/song.flac")
|
||||||
|
}
|
||||||
|
if d.Extension != "flac" {
|
||||||
|
t.Fatalf("extension = %q, want flac", d.Extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user