package soundcloud import ( "context" "fmt" "net/http" "net/http/httptest" "strings" "testing" "streamrip-go/internal/jsonutil" "streamrip-go/internal/config" ) func TestGetTrackMetadataAndDownloadable(t *testing.T) { cfgData := config.DefaultConfigData() c := New(&config.Config{File: cfgData, Session: cfgData}) c.loggedIn = true c.run = func(_ context.Context, _ string, args ...string) ([]byte, error) { joined := strings.Join(args, " ") if strings.Contains(joined, "--no-playlist") { return []byte(`{"title":"Lean On","uploader":"Major Lazer","url":"https://cdn.example/audio.m4a","ext":"m4a","thumbnail":"https://img.example/cover.jpg"}`), nil } return nil, fmt.Errorf("unexpected args: %v", args) } meta, err := c.GetMetadata(context.Background(), "https://soundcloud.com/a/b", "track") if err != nil { t.Fatalf("GetMetadata() error = %v", err) } if jsonutil.StringFromAny(meta["title"]) != "Lean On" { t.Fatalf("title = %q, want Lean On", jsonutil.StringFromAny(meta["title"])) } if jsonutil.StringFromAny(meta["id"]) != "https://soundcloud.com/a/b" { t.Fatalf("id = %q, want canonical soundcloud url", jsonutil.StringFromAny(meta["id"])) } d, err := c.GetDownloadable(context.Background(), "https://soundcloud.com/a/b", 0) if err != nil { t.Fatalf("GetDownloadable() error = %v", err) } if d.URL != "https://cdn.example/audio.m4a" || d.Extension != "m4a" { t.Fatalf("unexpected downloadable: %+v", d) } } func TestGetPlaylistMetadata(t *testing.T) { cfgData := config.DefaultConfigData() c := New(&config.Config{File: cfgData, Session: cfgData}) c.loggedIn = true c.run = func(_ context.Context, _ string, args ...string) ([]byte, error) { joined := strings.Join(args, " ") if strings.Contains(joined, "--skip-download") && !strings.Contains(joined, "--no-playlist") { return []byte(`{"title":"Road Trip","entries":[{"webpage_url":"https://soundcloud.com/a/t1"},{"url":"https://soundcloud.com/a/t2"}]}`), nil } return nil, fmt.Errorf("unexpected args: %v", args) } meta, err := c.GetMetadata(context.Background(), "https://soundcloud.com/a/sets/road-trip", "playlist") if err != nil { t.Fatalf("GetMetadata() error = %v", err) } if jsonutil.StringFromAny(meta["name"]) != "Road Trip" { t.Fatalf("name = %q, want Road Trip", jsonutil.StringFromAny(meta["name"])) } tracksMap, ok := meta["tracks"].(map[string]any) if !ok { t.Fatalf("tracks missing") } items := asAnySlice(tracksMap["items"]) if len(items) != 2 { t.Fatalf("playlist items len = %d, want 2", len(items)) } if jsonutil.StringFromAny(meta["id"]) != "https://soundcloud.com/a/sets/road-trip" { t.Fatalf("playlist id not canonical: %q", jsonutil.StringFromAny(meta["id"])) } } func TestSearchTrack(t *testing.T) { cfgData := config.DefaultConfigData() c := New(&config.Config{File: cfgData, Session: cfgData}) c.loggedIn = true c.run = func(_ context.Context, _ string, args ...string) ([]byte, error) { joined := strings.Join(args, " ") if strings.Contains(joined, "scsearch2:lean on") { return []byte(`{"entries":[{"title":"Lean On","uploader":"Major Lazer","webpage_url":"https://soundcloud.com/a/b"}]}`), nil } return nil, fmt.Errorf("unexpected args: %v", args) } pages, err := c.Search(context.Background(), "track", "lean on", 2) if err != nil { t.Fatalf("Search() error = %v", err) } if len(pages) != 1 { t.Fatalf("pages len = %d, want 1", len(pages)) } items := asAnySlice(pages[0]["items"]) if len(items) != 1 { t.Fatalf("items len = %d, want 1", len(items)) } item0, ok := items[0].(map[string]any) if !ok { t.Fatalf("expected first item map") } if jsonutil.StringFromAny(item0["id"]) != "https://soundcloud.com/a/b" { t.Fatalf("track search id not canonical: %q", jsonutil.StringFromAny(item0["id"])) } } func TestSearchPlaylist(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/search/sets" { _, _ = w.Write([]byte(`
x`)) return } w.WriteHeader(http.StatusNotFound) })) defer ts.Close() cfgData := config.DefaultConfigData() c := New(&config.Config{File: cfgData, Session: cfgData}) c.loggedIn = true c.http = ts.Client() origBase := soundcloudSearchBaseURL soundcloudSearchBaseURL = ts.URL defer func() { soundcloudSearchBaseURL = origBase }() c.run = func(_ context.Context, _ string, args ...string) ([]byte, error) { joined := strings.Join(args, " ") if strings.Contains(joined, "https://soundcloud.com/a/sets/road-trip") { return []byte(`{"title":"Road Trip","uploader":"User","entries":[{"webpage_url":"https://soundcloud.com/a/t1"}]}`), nil } return nil, fmt.Errorf("unexpected args: %v", args) } pages, err := c.Search(context.Background(), "playlist", "road trip", 5) if err != nil { t.Fatalf("Search() error = %v", err) } if len(pages) != 1 { t.Fatalf("pages len = %d, want 1", len(pages)) } items := asAnySlice(pages[0]["items"]) if len(items) != 1 { t.Fatalf("items len = %d, want 1", len(items)) } item0, ok := items[0].(map[string]any) if !ok { t.Fatalf("expected first item map") } if jsonutil.StringFromAny(item0["id"]) != "https://soundcloud.com/a/sets/road-trip" { t.Fatalf("playlist search id not canonical: %q", jsonutil.StringFromAny(item0["id"])) } } func TestSearchPlaylistAcceptsDotsInPath(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/search/sets" { _, _ = w.Write([]byte(`x`)) return } w.WriteHeader(http.StatusNotFound) })) defer ts.Close() cfgData := config.DefaultConfigData() c := New(&config.Config{File: cfgData, Session: cfgData}) c.loggedIn = true c.http = ts.Client() origBase := soundcloudSearchBaseURL soundcloudSearchBaseURL = ts.URL defer func() { soundcloudSearchBaseURL = origBase }() c.run = func(_ context.Context, _ string, args ...string) ([]byte, error) { joined := strings.Join(args, " ") if strings.Contains(joined, "https://soundcloud.com/artist.name/sets/road.trip") { return []byte(`{"title":"Road Trip","uploader":"User","entries":[{"webpage_url":"https://soundcloud.com/a/t1"}]}`), nil } return nil, fmt.Errorf("unexpected args: %v", args) } pages, err := c.Search(context.Background(), "playlist", "road trip", 5) if err != nil { t.Fatalf("Search() error = %v", err) } if len(pages) != 1 { t.Fatalf("pages len = %d, want 1", len(pages)) } items := asAnySlice(pages[0]["items"]) if len(items) != 1 { t.Fatalf("items len = %d, want 1", len(items)) } item0, ok := items[0].(map[string]any) if !ok { t.Fatalf("expected first item map") } if jsonutil.StringFromAny(item0["id"]) != "https://soundcloud.com/artist.name/sets/road.trip" { t.Fatalf("playlist search id not canonical: %q", jsonutil.StringFromAny(item0["id"])) } } func TestLoginShowsYtDlpHint(t *testing.T) { cfgData := config.DefaultConfigData() c := New(&config.Config{File: cfgData, Session: cfgData}) c.bin = "definitely-not-a-real-yt-dlp-bin" err := c.Login(context.Background()) if err == nil { t.Fatalf("expected login error") } if !strings.Contains(strings.ToLower(err.Error()), "yt-dlp is required") { t.Fatalf("expected yt-dlp hint in error, got: %v", err) } } func TestTrackMetadataIncludesExplicitAndISRC(t *testing.T) { meta := trackMetadataFromInfo("https://soundcloud.com/a/b", map[string]any{ "title": "T", "uploader": "U", "isrc": "US123", "id": "9876", "webpage_url": "https://soundcloud.com/a/b?si=abc", "age_limit": float64(18), "thumbnail": "https://img", "upload_date": "20240101", }) if jsonutil.StringFromAny(meta["isrc"]) != "US123" { t.Fatalf("isrc = %q, want US123", jsonutil.StringFromAny(meta["isrc"])) } explicit, _ := meta["explicit"].(bool) if !explicit { t.Fatalf("expected explicit=true") } if jsonutil.StringFromAny(meta["source_track_id"]) != "9876" { t.Fatalf("source_track_id = %q, want 9876", jsonutil.StringFromAny(meta["source_track_id"])) } if jsonutil.StringFromAny(jsonutil.NestedMap(meta, "album")["title"]) != "T" { t.Fatalf("album title mismatch: %#v", jsonutil.NestedMap(meta, "album")) } } func TestCanonicalSoundcloudURL(t *testing.T) { got := canonicalSoundcloudURL(map[string]any{"webpage_url": "https://soundcloud.com/a/b/?si=x#frag"}) if got != "https://soundcloud.com/a/b" { t.Fatalf("canonical url = %q, want %q", got, "https://soundcloud.com/a/b") } } func TestCanonicalSoundcloudURLAcceptsSubdomain(t *testing.T) { got := canonicalSoundcloudURL(map[string]any{"webpage_url": "https://m.soundcloud.com/a/b/?si=x#frag"}) if got != "https://soundcloud.com/a/b" { t.Fatalf("canonical url = %q, want %q", got, "https://soundcloud.com/a/b") } }