mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
388 lines
12 KiB
Go
388 lines
12 KiB
Go
package deezer
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"streamrip-go/internal/jsonutil"
|
|
|
|
"streamrip-go/internal/config"
|
|
)
|
|
|
|
func TestSearchTrack(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/search/track" {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"id": 1, "title": "Dreams", "artist": map[string]any{"name": "Fleetwood Mac"}}}})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.loggedIn = true
|
|
|
|
origBase := baseURL
|
|
baseURL = ts.URL
|
|
defer func() { baseURL = origBase }()
|
|
|
|
pages, err := c.Search(context.Background(), "track", "dreams", 5)
|
|
if err != nil {
|
|
t.Fatalf("Search() error = %v", err)
|
|
}
|
|
if len(pages) != 1 {
|
|
t.Fatalf("pages len = %d, want 1", len(pages))
|
|
}
|
|
}
|
|
|
|
func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
|
|
callCount := 0
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/artist/9":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 9, "name": "Lost Frequencies"})
|
|
case "/artist/9/albums":
|
|
callCount++
|
|
index := r.URL.Query().Get("index")
|
|
limit := r.URL.Query().Get("limit")
|
|
if limit != "100" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "bad limit"}})
|
|
return
|
|
}
|
|
switch index {
|
|
case "0":
|
|
items := make([]any, 0, 100)
|
|
for i := 0; i < 100; i++ {
|
|
items = append(items, map[string]any{"id": i + 1, "title": "Album"})
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": items, "total": 101})
|
|
case "100":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"id": 101, "title": "Album 101"}}, "total": 101})
|
|
default:
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.loggedIn = true
|
|
|
|
origBase := baseURL
|
|
baseURL = ts.URL
|
|
defer func() { baseURL = origBase }()
|
|
|
|
meta, err := c.GetMetadata(context.Background(), "9", "artist")
|
|
if err != nil {
|
|
t.Fatalf("GetMetadata() error = %v", err)
|
|
}
|
|
albumsObj, _ := meta["albums"].(map[string]any)
|
|
items, _ := albumsObj["items"].([]any)
|
|
if len(items) != 101 {
|
|
t.Fatalf("albums len = %d, want 101", len(items))
|
|
}
|
|
if got := strings.TrimSpace(jsonutil.StringFromAny(meta["name"])); got != "Lost Frequencies" {
|
|
t.Fatalf("artist name = %q, want Lost Frequencies", got)
|
|
}
|
|
if callCount != 2 {
|
|
t.Fatalf("call count = %d, want 2", callCount)
|
|
}
|
|
}
|
|
|
|
func TestGetDownloadableNativeCipher(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/track/42":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "track_token": "tt"})
|
|
case "/media":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"errors": []any{}, "media": []any{map[string]any{"cipher": map[string]any{"type": "BF_CBC_STRIPE"}, "format": "FLAC", "sources": []any{map[string]any{"url": "https://cdn.example/file"}}}}}}})
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
cfgData.Deezer.ARL = "arl"
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.loggedIn = true
|
|
c.arl = "arl"
|
|
c.license = "license"
|
|
c.jwt = "jwt"
|
|
|
|
origBase := baseURL
|
|
origMedia := mediaURL
|
|
origPipe := pipeURL
|
|
baseURL = ts.URL
|
|
mediaURL = ts.URL + "/media"
|
|
pipeURL = ts.URL + "/pipe"
|
|
defer func() {
|
|
baseURL = origBase
|
|
mediaURL = origMedia
|
|
pipeURL = origPipe
|
|
}()
|
|
|
|
d, err := c.GetDownloadable(context.Background(), "42", 2)
|
|
if err != nil {
|
|
t.Fatalf("GetDownloadable() error = %v", err)
|
|
}
|
|
if d.Cipher != "BF_CBC_STRIPE" || d.Extension != "flac" || d.TrackID != "42" {
|
|
t.Fatalf("unexpected downloadable: %+v", d)
|
|
}
|
|
if d.Audio.Container != "FLAC" || d.Audio.Quality != "LOSSLESS" {
|
|
t.Fatalf("unexpected audio profile: %+v", d.Audio)
|
|
}
|
|
}
|
|
|
|
func TestGetDownloadableRequiresARL(t *testing.T) {
|
|
cfgData := config.DefaultConfigData()
|
|
cfgData.Deezer.ARL = ""
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.loggedIn = true
|
|
_, err := c.GetDownloadable(context.Background(), "42", 2)
|
|
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "arl") {
|
|
t.Fatalf("expected arl requirement error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetDownloadableDRMError(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/track/42":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 42, "title": "X", "track_token": "tt"})
|
|
case "/media":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": []any{map[string]any{"errors": []any{map[string]any{"code": 403, "message": "DRM required"}}, "media": []any{}}}})
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
cfgData.Deezer.ARL = "arl"
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.loggedIn = true
|
|
c.arl = "arl"
|
|
c.license = "license"
|
|
c.jwt = "jwt"
|
|
|
|
origBase := baseURL
|
|
origMedia := mediaURL
|
|
origPipe := pipeURL
|
|
baseURL = ts.URL
|
|
mediaURL = ts.URL + "/media"
|
|
pipeURL = ts.URL + "/pipe"
|
|
defer func() {
|
|
baseURL = origBase
|
|
mediaURL = origMedia
|
|
pipeURL = origPipe
|
|
}()
|
|
|
|
_, err := c.GetDownloadable(context.Background(), "42", 2)
|
|
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "drm") {
|
|
t.Fatalf("expected drm error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestGetMetadataAddsLyricsFromPipe(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/track/1141668":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 1141668, "title": "In Da Club", "artist": map[string]any{"name": "50 Cent"}})
|
|
case "/pipe":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"track": map[string]any{"lyrics": map[string]any{"text": "Go, go, go\nGo shawty", "synchronizedLines": []any{map[string]any{"line": "Go, go, go", "milliseconds": 0}, map[string]any{"line": "Go shawty", "milliseconds": 4280}}}}}})
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.loggedIn = true
|
|
c.jwt = "jwt"
|
|
|
|
origBase := baseURL
|
|
origPipe := pipeURL
|
|
baseURL = ts.URL
|
|
pipeURL = ts.URL + "/pipe"
|
|
defer func() {
|
|
baseURL = origBase
|
|
pipeURL = origPipe
|
|
}()
|
|
|
|
meta, err := c.GetMetadata(context.Background(), "1141668", "track")
|
|
if err != nil {
|
|
t.Fatalf("GetMetadata() error = %v", err)
|
|
}
|
|
if !strings.Contains(jsonutil.StringFromAny(meta["lyrics"]), "Go shawty") {
|
|
t.Fatalf("expected lyrics text, got %q", jsonutil.StringFromAny(meta["lyrics"]))
|
|
}
|
|
if !strings.Contains(jsonutil.StringFromAny(meta["lyrics_synced"]), "[00:00.00]Go, go, go") {
|
|
t.Fatalf("expected synced lyrics, got %q", jsonutil.StringFromAny(meta["lyrics_synced"]))
|
|
}
|
|
}
|
|
|
|
func TestLoginWithCredentials(t *testing.T) {
|
|
mobileToken := testMobileToken(t)
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/gateway" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
switch r.URL.Query().Get("method") {
|
|
case "mobile_auth":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"TOKEN": mobileToken}})
|
|
case "api_checkToken":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": "sid123"})
|
|
case "mobile_userAuth":
|
|
var payload map[string]any
|
|
_ = json.NewDecoder(r.Body).Decode(&payload)
|
|
if strings.TrimSpace(jsonutil.StringFromAny(payload["mail"])) == "" || strings.TrimSpace(jsonutil.StringFromAny(payload["password"])) == "" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "missing creds"}})
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"results": map[string]any{"ARL": "arl-token", "JWT": "jwt-token", "refresh_token": "refresh-token", "license_token": "license-token", "USER_ID": "42"}})
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
cfgData.Deezer.Email = "tidal1@alpin.sbs"
|
|
cfgData.Deezer.Password = "tidal1@alpin.sbs"
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
|
|
origGateway := gatewayURL
|
|
gatewayURL = ts.URL + "/gateway"
|
|
defer func() { gatewayURL = origGateway }()
|
|
|
|
if err := c.Login(context.Background()); err != nil {
|
|
t.Fatalf("Login() error = %v", err)
|
|
}
|
|
if !c.loggedIn {
|
|
t.Fatalf("expected logged in client")
|
|
}
|
|
if c.arl != "arl-token" {
|
|
t.Fatalf("arl = %q, want arl-token", c.arl)
|
|
}
|
|
if c.jwt != "jwt-token" {
|
|
t.Fatalf("jwt = %q, want jwt-token", c.jwt)
|
|
}
|
|
if c.refresh != "refresh-token" {
|
|
t.Fatalf("refresh = %q, want refresh-token", c.refresh)
|
|
}
|
|
if c.license != "license-token" {
|
|
t.Fatalf("license = %q, want license-token", c.license)
|
|
}
|
|
if c.cfg.Session.Deezer.RefreshToken != "refresh-token" {
|
|
t.Fatalf("session refresh token = %q", c.cfg.Session.Deezer.RefreshToken)
|
|
}
|
|
if c.cfg.File.Deezer.RefreshToken != "refresh-token" {
|
|
t.Fatalf("file refresh token = %q", c.cfg.File.Deezer.RefreshToken)
|
|
}
|
|
}
|
|
|
|
func testMobileToken(t *testing.T) string {
|
|
t.Helper()
|
|
plain := []byte(strings.Repeat("A", 64) + strings.Repeat("B", 16) + strings.Repeat("C", 16))
|
|
enc, err := aesECBEncrypt([]byte(gatewayDec), plain)
|
|
if err != nil {
|
|
t.Fatalf("aesECBEncrypt() error = %v", err)
|
|
}
|
|
return hex.EncodeToString(enc)
|
|
}
|
|
|
|
func TestLoginWithRefreshToken(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/renew":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"jwt": "jwt-token", "refresh_token": "refresh-token-2"})
|
|
case "/pipe":
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"tokens": map[string]any{"mediaServiceLicenseToken": map[string]any{"token": "license-token"}}}})
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
cfgData.Deezer.RefreshToken = "refresh-token"
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
|
|
origAuth := authURL
|
|
origPipe := pipeURL
|
|
authURL = ts.URL + "/renew"
|
|
pipeURL = ts.URL + "/pipe"
|
|
defer func() {
|
|
authURL = origAuth
|
|
pipeURL = origPipe
|
|
}()
|
|
|
|
if err := c.Login(context.Background()); err != nil {
|
|
t.Fatalf("Login() error = %v", err)
|
|
}
|
|
if !c.loggedIn {
|
|
t.Fatalf("expected logged in client")
|
|
}
|
|
if c.jwt != "jwt-token" || c.license != "license-token" {
|
|
t.Fatalf("unexpected jwt/license: jwt=%q license=%q", c.jwt, c.license)
|
|
}
|
|
if c.cfg.Session.Deezer.RefreshToken != "refresh-token-2" {
|
|
t.Fatalf("session refresh token = %q", c.cfg.Session.Deezer.RefreshToken)
|
|
}
|
|
}
|
|
|
|
func TestRefreshJWTHTTPError(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"error": map[string]any{"message": "bad refresh"}})
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.refresh = "refresh-token"
|
|
|
|
origAuth := authURL
|
|
authURL = ts.URL
|
|
defer func() { authURL = origAuth }()
|
|
|
|
err := c.refreshJWT(context.Background())
|
|
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "status=401") {
|
|
t.Fatalf("expected http status error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRefreshLicenseFromPipeGraphQLError(t *testing.T) {
|
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"errors": []any{map[string]any{"message": "token expired"}}})
|
|
}))
|
|
defer ts.Close()
|
|
|
|
cfgData := config.DefaultConfigData()
|
|
c := New(&config.Config{File: cfgData, Session: cfgData})
|
|
c.jwt = "jwt-token"
|
|
|
|
origPipe := pipeURL
|
|
pipeURL = ts.URL
|
|
defer func() { pipeURL = origPipe }()
|
|
|
|
err := c.refreshLicenseFromPipe(context.Background())
|
|
if err == nil || !strings.Contains(strings.ToLower(err.Error()), "token expired") {
|
|
t.Fatalf("expected graphql error, got %v", err)
|
|
}
|
|
}
|