Files
streamrip-go/internal/provider/tidal/client_test.go

246 lines
8.0 KiB
Go

package tidal
import (
"context"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"streamrip-go/internal/config"
)
func TestLoginMissingToken(t *testing.T) {
cfgData := config.DefaultConfigData()
cfgData.Tidal.AccessToken = ""
c := New(&config.Config{File: cfgData, Session: cfgData})
err := c.Login(context.Background())
if err == nil {
t.Fatalf("expected error")
}
}
func TestSearch(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/sessions":
_ = json.NewEncoder(w).Encode(map[string]any{"countryCode": "US", "userId": 123})
case "/v1/search/albums":
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{map[string]any{"id": 1, "title": "x"}}})
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.baseURL = ts.URL + "/v1"
if err := c.Login(context.Background()); err != nil {
t.Fatalf("login err = %v", err)
}
pages, err := c.Search(context.Background(), "album", "x", 10)
if err != nil {
t.Fatalf("search err = %v", err)
}
if len(pages) != 1 {
t.Fatalf("pages = %d", len(pages))
}
}
func TestGetVideoDownloadable(t *testing.T) {
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/sessions":
_ = json.NewEncoder(w).Encode(map[string]any{"countryCode": "US", "userId": 123})
case "/v1/videos/42/playbackinfopostpaywall":
manifest := map[string]any{"urls": []string{server.URL + "/master.m3u8"}}
b, _ := json.Marshal(manifest)
_ = json.NewEncoder(w).Encode(map[string]any{"manifest": base64.StdEncoding.EncodeToString(b)})
case "/master.m3u8":
_, _ = w.Write([]byte("#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=1000,CODECS=\"avc1.42E01E,mp4a.40.2\",RESOLUTION=640x360\nlow/stream.m3u8\n#EXT-X-STREAM-INF:BANDWIDTH=2000,CODECS=\"avc1.4D401F,mp4a.40.2\",RESOLUTION=1280x720\nhi/stream.m3u8\n"))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
cfgData := config.DefaultConfigData()
cfgData.Tidal.AccessToken = "token"
cfgData.Tidal.CountryCode = "US"
c := New(&config.Config{File: cfgData, Session: cfgData})
c.baseURL = server.URL + "/v1"
if err := c.Login(context.Background()); err != nil {
t.Fatalf("login err = %v", err)
}
d, err := c.GetVideoDownloadable(context.Background(), "42")
if err != nil {
t.Fatalf("GetVideoDownloadable() err = %v", err)
}
if d.Extension != "mp4" {
t.Fatalf("extension = %q, want mp4", d.Extension)
}
if d.URL != server.URL+"/hi/stream.m3u8" {
t.Fatalf("url = %q, want %q", d.URL, server.URL+"/hi/stream.m3u8")
}
}
func TestBestHLSVariantURLFallsBackToMaster(t *testing.T) {
master := "https://example.com/master.m3u8"
got := bestHLSVariantURL(master, "#EXTM3U\n#comment")
if got != master {
t.Fatalf("url = %q, want %q", got, master)
}
}
func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/sessions":
_ = json.NewEncoder(w).Encode(map[string]any{"countryCode": "US", "userId": 123})
case "/v1/artists/9":
_ = json.NewEncoder(w).Encode(map[string]any{"id": 9, "name": "Artist X"})
case "/v1/artists/9/albums":
offset, _ := strconv.Atoi(r.URL.Query().Get("offset"))
filter := r.URL.Query().Get("filter")
if filter == "" {
if offset == 0 {
items := make([]any, 0, 100)
for i := 0; i < 100; i++ {
items = append(items, map[string]any{"id": i + 1})
}
_ = json.NewEncoder(w).Encode(map[string]any{"items": items})
return
}
if offset == 100 {
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{map[string]any{"id": 101}}})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{}})
return
}
if filter == "EPSANDSINGLES" {
if offset == 0 {
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{map[string]any{"id": 101}, map[string]any{"id": 102}}})
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{}})
return
}
w.WriteHeader(http.StatusBadRequest)
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.baseURL = ts.URL + "/v1"
if err := c.Login(context.Background()); err != nil {
t.Fatalf("login err = %v", err)
}
meta, err := c.GetMetadata(context.Background(), "9", "artist")
if err != nil {
t.Fatalf("GetMetadata() err = %v", err)
}
albumsObj, _ := meta["albums"].(map[string]any)
items, _ := albumsObj["items"].([]map[string]any)
if len(items) != 102 {
t.Fatalf("albums len = %d, want 102", len(items))
}
}
func TestGetDownloadablePrefersAtmosWhenEnabled(t *testing.T) {
var calls []string
allImmersive := true
var ts *httptest.Server
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, "audioModes": []any{"DOLBY_ATMOS", "STEREO"}, "mediaMetadata": map[string]any{"tags": []any{"LOSSLESS", "DOLBY_ATMOS"}}})
case "/v1/tracks/42/playbackinfopostpaywall":
if r.URL.Query().Get("immersiveaudio") != "true" {
allImmersive = false
}
aq := r.URL.Query().Get("audioquality")
calls = append(calls, aq)
manifest := map[string]any{"urls": []string{ts.URL + "/stereo.m3u8"}, "codecs": "flac"}
if aq == "HI_RES" {
manifest = map[string]any{"urls": []string{ts.URL + "/atmos.m3u8"}, "codecs": "ec-3", "audioMode": "DOLBY_ATMOS"}
}
b, _ := json.Marshal(manifest)
_ = json.NewEncoder(w).Encode(map[string]any{"manifest": base64.StdEncoding.EncodeToString(b), "audioMode": manifest["audioMode"]})
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer ts.Close()
cfgData := config.DefaultConfigData()
cfgData.Tidal.AccessToken = "token"
cfgData.Tidal.CountryCode = "US"
cfgData.Tidal.PreferAtmos = true
c := New(&config.Config{File: cfgData, Session: cfgData})
c.loggedIn = true
c.baseURL = ts.URL + "/v1"
d, err := c.GetDownloadable(context.Background(), "42", 3)
if err != nil {
t.Fatalf("GetDownloadable() err = %v", err)
}
if d.URL != ts.URL+"/atmos.m3u8" {
t.Fatalf("url = %q, want %q", d.URL, ts.URL+"/atmos.m3u8")
}
if d.Extension != "mka" {
t.Fatalf("extension = %q, want mka", d.Extension)
}
if len(calls) < 2 || calls[0] != "HI_RES_LOSSLESS" || calls[1] != "HI_RES" {
t.Fatalf("unexpected audioquality call order: %+v", calls)
}
if !allImmersive {
t.Fatalf("expected immersiveaudio=true on Atmos probing calls")
}
}
func TestPlaybackLooksAtmosFromManifest(t *testing.T) {
manifest := map[string]any{"urls": []string{"https://cdn.example/stream.m3u8"}, "codecs": "ec-3"}
b, _ := json.Marshal(manifest)
resp := map[string]any{"manifest": base64.StdEncoding.EncodeToString(b)}
if !playbackLooksAtmos(resp) {
t.Fatalf("expected atmos detection from manifest codec")
}
}
func TestTrackSupportsAtmosFromTags(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/tracks/42" {
w.WriteHeader(http.StatusNotFound)
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "audioModes": []any{"STEREO"}, "mediaMetadata": map[string]any{"tags": []any{"LOSSLESS", "DOLBY_ATMOS"}}})
}))
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"
if !c.trackSupportsAtmos(context.Background(), "42") {
t.Fatalf("expected atmos support from mediaMetadata tags")
}
}