mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-10-16 22:07:30 +02:00
refactor: delete dead code, dedupe helpers, add tests for untested packages
Dead code Removed, each verified to have no non-test caller: promptSearchSelection (77 lines, superseded by promptSearchSelectionMenu), AddPlaylistByTrackIDs, download.NewWithVerifySSL, qobuz.Client.GetTrackMetadata, tidal.Client.trackSupportsAtmos (test-only, and the comment above GetDownloadable already documented its removal from the live path) and its now-orphaned test, yandex.canonicalTrackRequestID (a no-op returning its trimmed input; the call site uses strings.TrimSpace directly) and the unused defaultEstimatedKbps constant. Config keys that did nothing qobuz.download_booklets and misc.check_for_updates were declared, defaulted and documented in config.toml.example but read by no code. The whole [youtube] block went too: there is no YouTube provider. go-toml ignores unknown keys, verified, so existing user configs still load. Duplicated helpers tidal.stringify and yandex.firstNonEmpty were byte-identical copies of jsonutil.StringFromAny and jsonutil.FirstNonEmpty. displayArtistNames existed three times, byte-identical, in internal/app, provider/yandex and provider/beatport; it now lives in jsonutil. The CLI's normalizeCodec delegates to config.NormalizeCodec so the flag and the config file are checked by the same rules. Tests for packages that had none internal/jsonutil (0% -> 100%) and internal/netutil (0% -> 78%), the latter covering redactURL, which is a security control, and the zero-timeout contract that internal/download and the qobuz body copy depend on. internal/config rose to 82% with coverage of the new Validate(), the 0600 permissions, CloneConfigData not sharing slices, and a concurrent UpdateTokens test that exercises the mutex under -race. Test suite is ~85x faster Provider tests built clients with the default 60 requests/minute, so the rate limiter slept 1s per HTTP call: deezer took 31s, tidal 11s, qobuz 8s. Each provider test package now builds its config through a testConfigData helper that disables throttling. Whole suite: ~60s -> 0.7s, and 2.8s under -race. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
19 files changed
+701
-392
No files matched your search
@@ -120,10 +120,10 @@ func (c *Client) Login(ctx context.Context) error {
|
||||
return fmt.Errorf("tidal login failed: status=%d body=%v", status, resp)
|
||||
}
|
||||
|
||||
if v := stringify(resp["countryCode"]); v != "" {
|
||||
if v := jsonutil.StringFromAny(resp["countryCode"]); v != "" {
|
||||
c.cfg.Session.Tidal.CountryCode = v
|
||||
}
|
||||
if v := stringify(resp["userId"]); v != "" {
|
||||
if v := jsonutil.StringFromAny(resp["userId"]); v != "" {
|
||||
c.cfg.Session.Tidal.UserID = v
|
||||
}
|
||||
|
||||
@@ -159,12 +159,12 @@ func (c *Client) refreshAccessToken(ctx context.Context) error {
|
||||
return fmt.Errorf("tidal token refresh failed: status=%d body=%v", status, resp)
|
||||
}
|
||||
|
||||
newToken := stringify(resp["access_token"])
|
||||
newToken := jsonutil.StringFromAny(resp["access_token"])
|
||||
if newToken == "" {
|
||||
return errors.New("tidal token refresh missing access_token")
|
||||
}
|
||||
|
||||
newRefresh := stringify(resp["refresh_token"])
|
||||
newRefresh := jsonutil.StringFromAny(resp["refresh_token"])
|
||||
expiresIn := int64(jsonutil.IntFromAny(resp["expires_in"]))
|
||||
if expiresIn <= 0 {
|
||||
expiresIn = 7 * 24 * 3600
|
||||
@@ -263,8 +263,8 @@ func (c *Client) fetchTrackLyrics(ctx context.Context, trackID string) (string,
|
||||
return "", ""
|
||||
}
|
||||
|
||||
lyrics := strings.TrimSpace(stringify(resp["lyrics"]))
|
||||
lrc := strings.TrimSpace(stringify(resp["subtitles"]))
|
||||
lyrics := strings.TrimSpace(jsonutil.StringFromAny(resp["lyrics"]))
|
||||
lrc := strings.TrimSpace(jsonutil.StringFromAny(resp["subtitles"]))
|
||||
return lyrics, lrc
|
||||
}
|
||||
|
||||
@@ -339,30 +339,6 @@ func (c *Client) GetDownloadable(ctx context.Context, trackID string, quality in
|
||||
return c.getDownloadableFromTrackManifest(ctx, trackID, quality)
|
||||
}
|
||||
|
||||
func (c *Client) trackSupportsAtmos(ctx context.Context, trackID string) bool {
|
||||
resp, status, err := c.apiRequest(ctx, "tracks/"+trackID, url.Values{}, c.baseURL)
|
||||
if err != nil || status != http.StatusOK {
|
||||
return false
|
||||
}
|
||||
if modes, ok := resp["audioModes"].([]any); ok {
|
||||
for _, mode := range modes {
|
||||
if strings.Contains(strings.ToUpper(stringify(mode)), "ATMOS") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if mm, ok := resp["mediaMetadata"].(map[string]any); ok {
|
||||
if tags, ok := mm["tags"].([]any); ok {
|
||||
for _, tag := range tags {
|
||||
if strings.Contains(strings.ToUpper(stringify(tag)), "ATMOS") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Client) getAtmosDownloadable(ctx context.Context, trackID string) (*provider.Downloadable, error) {
|
||||
var lastErr error
|
||||
for _, aq := range atmosAudioQualities {
|
||||
@@ -407,18 +383,18 @@ func (c *Client) getAtmosDownloadable(ctx context.Context, trackID string) (*pro
|
||||
}
|
||||
|
||||
func playbackLooksAtmos(resp map[string]any) bool {
|
||||
if strings.Contains(strings.ToUpper(stringify(resp["audioMode"])), "ATMOS") {
|
||||
if strings.Contains(strings.ToUpper(jsonutil.StringFromAny(resp["audioMode"])), "ATMOS") {
|
||||
return true
|
||||
}
|
||||
if modes, ok := resp["audioModes"].([]any); ok {
|
||||
for _, raw := range modes {
|
||||
if strings.Contains(strings.ToUpper(stringify(raw)), "ATMOS") {
|
||||
if strings.Contains(strings.ToUpper(jsonutil.StringFromAny(raw)), "ATMOS") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
manifestB64 := stringify(resp["manifest"])
|
||||
manifestB64 := jsonutil.StringFromAny(resp["manifest"])
|
||||
if manifestB64 == "" {
|
||||
return false
|
||||
}
|
||||
@@ -430,17 +406,17 @@ func playbackLooksAtmos(resp map[string]any) bool {
|
||||
if err = json.Unmarshal(b, &manifest); err != nil {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(strings.ToUpper(stringify(manifest["audioMode"])), "ATMOS") {
|
||||
if strings.Contains(strings.ToUpper(jsonutil.StringFromAny(manifest["audioMode"])), "ATMOS") {
|
||||
return true
|
||||
}
|
||||
if modes, ok := manifest["audioModes"].([]any); ok {
|
||||
for _, raw := range modes {
|
||||
if strings.Contains(strings.ToUpper(stringify(raw)), "ATMOS") {
|
||||
if strings.Contains(strings.ToUpper(jsonutil.StringFromAny(raw)), "ATMOS") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
codec := strings.ToLower(stringify(manifest["codecs"]))
|
||||
codec := strings.ToLower(jsonutil.StringFromAny(manifest["codecs"]))
|
||||
return strings.Contains(codec, "ec-3") || strings.Contains(codec, "eac3") || strings.Contains(codec, "joc") || strings.Contains(codec, "atmos")
|
||||
}
|
||||
|
||||
@@ -524,7 +500,7 @@ func (c *Client) fetchArtistAlbums(ctx context.Context, artistID string) ([]map[
|
||||
if wrapped, ok := itm["item"].(map[string]any); ok {
|
||||
itm = wrapped
|
||||
}
|
||||
id := stringify(itm["id"])
|
||||
id := jsonutil.StringFromAny(itm["id"])
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
@@ -576,14 +552,14 @@ func (c *Client) getDownloadableFromTrackManifestForFormats(ctx context.Context,
|
||||
if !ok {
|
||||
return nil, errors.New("tidal trackManifests missing attributes")
|
||||
}
|
||||
uri := stringify(attrs["uri"])
|
||||
uri := jsonutil.StringFromAny(attrs["uri"])
|
||||
if uri == "" {
|
||||
return nil, errors.New("tidal trackManifests missing uri")
|
||||
}
|
||||
attrFormats, _ := attrs["formats"].([]any)
|
||||
ext := "m4a"
|
||||
for _, f := range attrFormats {
|
||||
fv := strings.ToUpper(stringify(f))
|
||||
fv := strings.ToUpper(jsonutil.StringFromAny(f))
|
||||
if strings.Contains(fv, "FLAC") {
|
||||
ext = "flac"
|
||||
break
|
||||
@@ -630,7 +606,7 @@ func (c *Client) GetVideoDownloadable(ctx context.Context, videoID string) (*pro
|
||||
return nil, fmt.Errorf("tidal video playbackinfo failed: status=%d", status)
|
||||
}
|
||||
|
||||
manifestB64 := stringify(resp["manifest"])
|
||||
manifestB64 := jsonutil.StringFromAny(resp["manifest"])
|
||||
if manifestB64 == "" {
|
||||
return nil, errors.New("tidal video manifest missing")
|
||||
}
|
||||
@@ -646,7 +622,7 @@ func (c *Client) GetVideoDownloadable(ctx context.Context, videoID string) (*pro
|
||||
if !ok || len(urls) == 0 {
|
||||
return nil, errors.New("tidal video manifest urls missing")
|
||||
}
|
||||
masterURL := stringify(urls[0])
|
||||
masterURL := jsonutil.StringFromAny(urls[0])
|
||||
if masterURL == "" {
|
||||
return nil, errors.New("tidal video master url missing")
|
||||
}
|
||||
@@ -677,7 +653,7 @@ func (c *Client) GetVideoDownloadable(ctx context.Context, videoID string) (*pro
|
||||
}
|
||||
|
||||
func downloadableFromPlaybackManifest(resp map[string]any) *provider.Downloadable {
|
||||
manifestB64 := stringify(resp["manifest"])
|
||||
manifestB64 := jsonutil.StringFromAny(resp["manifest"])
|
||||
if manifestB64 == "" {
|
||||
return nil
|
||||
}
|
||||
@@ -693,11 +669,11 @@ func downloadableFromPlaybackManifest(resp map[string]any) *provider.Downloadabl
|
||||
if !ok || len(urls) == 0 {
|
||||
return nil
|
||||
}
|
||||
streamURL := stringify(urls[0])
|
||||
streamURL := jsonutil.StringFromAny(urls[0])
|
||||
if streamURL == "" {
|
||||
return nil
|
||||
}
|
||||
codec := strings.ToLower(stringify(manifest["codecs"]))
|
||||
codec := strings.ToLower(jsonutil.StringFromAny(manifest["codecs"]))
|
||||
ext := "m4a"
|
||||
if strings.Contains(codec, "flac") {
|
||||
ext = "flac"
|
||||
@@ -708,14 +684,14 @@ func downloadableFromPlaybackManifest(resp map[string]any) *provider.Downloadabl
|
||||
if profile.Container == "" {
|
||||
profile = tidalAudioProfileFromExtension(ext)
|
||||
}
|
||||
audioQuality := strings.ToUpper(strings.TrimSpace(stringify(resp["audioQuality"])))
|
||||
audioQuality := strings.ToUpper(strings.TrimSpace(jsonutil.StringFromAny(resp["audioQuality"])))
|
||||
if audioQuality == "" {
|
||||
audioQuality = strings.ToUpper(strings.TrimSpace(stringify(manifest["audioQuality"])))
|
||||
audioQuality = strings.ToUpper(strings.TrimSpace(jsonutil.StringFromAny(manifest["audioQuality"])))
|
||||
}
|
||||
if audioQuality != "" {
|
||||
profile = applyTidalAudioQuality(profile, audioQuality)
|
||||
}
|
||||
if strings.Contains(strings.ToUpper(stringify(resp["audioMode"])), "ATMOS") {
|
||||
if strings.Contains(strings.ToUpper(jsonutil.StringFromAny(resp["audioMode"])), "ATMOS") {
|
||||
profile = tidalAtmosAudioProfile()
|
||||
}
|
||||
return &provider.Downloadable{URL: streamURL, Extension: ext, Source: "tidal", Audio: profile}
|
||||
@@ -724,7 +700,7 @@ func downloadableFromPlaybackManifest(resp map[string]any) *provider.Downloadabl
|
||||
func tidalAudioProfileFromFormats(formats []any) provider.AudioProfile {
|
||||
best := provider.AudioProfile{}
|
||||
for _, raw := range formats {
|
||||
f := strings.ToUpper(strings.TrimSpace(stringify(raw)))
|
||||
f := strings.ToUpper(strings.TrimSpace(jsonutil.StringFromAny(raw)))
|
||||
switch {
|
||||
case strings.Contains(f, "EAC3") || strings.Contains(f, "JOC") || strings.Contains(f, "ATMOS"):
|
||||
return tidalAtmosAudioProfile()
|
||||
@@ -1003,28 +979,13 @@ func (c *Client) apiPost(ctx context.Context, endpoint string, form url.Values,
|
||||
})
|
||||
}
|
||||
|
||||
func stringify(v any) string {
|
||||
switch t := v.(type) {
|
||||
case string:
|
||||
return t
|
||||
case int:
|
||||
return strconv.Itoa(t)
|
||||
case int64:
|
||||
return strconv.FormatInt(t, 10)
|
||||
case float64:
|
||||
return strconv.FormatFloat(t, 'f', -1, 64)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func enrichTidalImage(meta map[string]any) {
|
||||
if _, ok := meta["image"].(map[string]any); ok {
|
||||
return
|
||||
}
|
||||
cover := stringify(meta["cover"])
|
||||
cover := jsonutil.StringFromAny(meta["cover"])
|
||||
if cover == "" {
|
||||
cover = stringify(meta["squareImage"])
|
||||
cover = jsonutil.StringFromAny(meta["squareImage"])
|
||||
}
|
||||
if cover == "" {
|
||||
return
|
||||
|
||||
@@ -12,10 +12,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"streamrip-go/internal/config"
|
||||
"streamrip-go/internal/jsonutil"
|
||||
)
|
||||
|
||||
func TestLoginMissingToken(t *testing.T) {
|
||||
cfgData := config.DefaultConfigData()
|
||||
cfgData := testConfigData()
|
||||
cfgData.Tidal.AccessToken = ""
|
||||
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||
err := c.Login(context.Background())
|
||||
@@ -37,7 +38,7 @@ func TestSearch(t *testing.T) {
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cfgData := config.DefaultConfigData()
|
||||
cfgData := testConfigData()
|
||||
cfgData.Tidal.AccessToken = "token"
|
||||
cfgData.Tidal.CountryCode = "US"
|
||||
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||
@@ -73,7 +74,7 @@ func TestGetVideoDownloadable(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
cfgData := config.DefaultConfigData()
|
||||
cfgData := testConfigData()
|
||||
cfgData.Tidal.AccessToken = "token"
|
||||
cfgData.Tidal.CountryCode = "US"
|
||||
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||
@@ -143,7 +144,7 @@ func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cfgData := config.DefaultConfigData()
|
||||
cfgData := testConfigData()
|
||||
cfgData.Tidal.AccessToken = "token"
|
||||
cfgData.Tidal.CountryCode = "US"
|
||||
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||
@@ -185,7 +186,7 @@ func TestGetMetadataTrackAddsLyricsAndSyncedLyrics(t *testing.T) {
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cfgData := config.DefaultConfigData()
|
||||
cfgData := testConfigData()
|
||||
cfgData.Tidal.AccessToken = "token"
|
||||
cfgData.Tidal.CountryCode = "MY"
|
||||
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||
@@ -197,10 +198,10 @@ func TestGetMetadataTrackAddsLyricsAndSyncedLyrics(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetMetadata() err = %v", err)
|
||||
}
|
||||
if got := stringify(meta["lyrics"]); got != "plain lyrics line" {
|
||||
if got := jsonutil.StringFromAny(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" {
|
||||
if got := jsonutil.StringFromAny(meta["lyrics_synced"]); got != "[00:00.00]plain lyrics line" {
|
||||
t.Fatalf("lyrics_synced = %q, want synced lrc", got)
|
||||
}
|
||||
}
|
||||
@@ -219,7 +220,7 @@ func TestGetMetadataTrackIgnoresLyricsEndpointFailure(t *testing.T) {
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cfgData := config.DefaultConfigData()
|
||||
cfgData := testConfigData()
|
||||
cfgData.Tidal.AccessToken = "token"
|
||||
cfgData.Tidal.CountryCode = "US"
|
||||
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||
@@ -257,7 +258,7 @@ func TestAPIRequestRetriesTooManyRequests(t *testing.T) {
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cfgData := config.DefaultConfigData()
|
||||
cfgData := testConfigData()
|
||||
cfgData.Downloads.RequestsPerMinute = 0
|
||||
cfgData.Tidal.AccessToken = "token"
|
||||
cfgData.Tidal.CountryCode = "US"
|
||||
@@ -274,8 +275,8 @@ func TestAPIRequestRetriesTooManyRequests(t *testing.T) {
|
||||
if calls != 2 {
|
||||
t.Fatalf("calls = %d, want 2", calls)
|
||||
}
|
||||
if stringify(resp["title"]) != "Song" {
|
||||
t.Fatalf("title = %q, want Song", stringify(resp["title"]))
|
||||
if jsonutil.StringFromAny(resp["title"]) != "Song" {
|
||||
t.Fatalf("title = %q, want Song", jsonutil.StringFromAny(resp["title"]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +298,7 @@ func TestAPIPostRetriesTooManyRequests(t *testing.T) {
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cfgData := config.DefaultConfigData()
|
||||
cfgData := testConfigData()
|
||||
cfgData.Downloads.RequestsPerMinute = 0
|
||||
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||
|
||||
@@ -311,8 +312,8 @@ func TestAPIPostRetriesTooManyRequests(t *testing.T) {
|
||||
if calls != 2 {
|
||||
t.Fatalf("calls = %d, want 2", calls)
|
||||
}
|
||||
if stringify(resp["access_token"]) != "fresh-token" {
|
||||
t.Fatalf("access_token = %q, want fresh-token", stringify(resp["access_token"]))
|
||||
if jsonutil.StringFromAny(resp["access_token"]) != "fresh-token" {
|
||||
t.Fatalf("access_token = %q, want fresh-token", jsonutil.StringFromAny(resp["access_token"]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +343,7 @@ func TestGetDownloadablePrefersAtmosWhenEnabled(t *testing.T) {
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cfgData := config.DefaultConfigData()
|
||||
cfgData := testConfigData()
|
||||
cfgData.Tidal.AccessToken = "token"
|
||||
cfgData.Tidal.CountryCode = "US"
|
||||
cfgData.Tidal.PreferAtmos = true
|
||||
@@ -377,28 +378,6 @@ func TestPlaybackLooksAtmosFromManifest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatsForQuality(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -448,7 +427,7 @@ func TestGetDownloadableLosslessUsesTrackManifestWhenPlaybackIsAAC(t *testing.T)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cfgData := config.DefaultConfigData()
|
||||
cfgData := testConfigData()
|
||||
cfgData.Tidal.AccessToken = "token"
|
||||
cfgData.Tidal.CountryCode = "US"
|
||||
c := New(&config.Config{File: cfgData, Session: cfgData})
|
||||
@@ -473,3 +452,13 @@ func TestGetDownloadableLosslessUsesTrackManifestWhenPlaybackIsAAC(t *testing.T)
|
||||
t.Fatalf("unexpected audio profile: %+v", d.Audio)
|
||||
}
|
||||
}
|
||||
|
||||
// testConfigData returns defaults with request throttling disabled.
|
||||
//
|
||||
// At the default 60 requests/minute the rate limiter sleeps 1s per request,
|
||||
// which accounted for most of this suite's runtime (deezer alone was 31s).
|
||||
func testConfigData() config.ConfigData {
|
||||
d := config.DefaultConfigData()
|
||||
d.Downloads.RequestsPerMinute = -1
|
||||
return d
|
||||
}
|
||||
Reference in new issue
Block a user