mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-10-16 22:07:30 +02:00
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>
148 lines
3.3 KiB
Go
148 lines
3.3 KiB
Go
// Package jsonutil provides shared helpers for working with untyped JSON
|
|
// values (map[string]any) that come from API responses across all providers.
|
|
package jsonutil
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// StringFromAny converts a dynamic JSON value to a string.
|
|
// Numeric types are formatted without trailing zeroes.
|
|
func StringFromAny(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 ""
|
|
}
|
|
}
|
|
|
|
// IntFromAny converts a dynamic JSON value to an int.
|
|
// Handles int, int64, float64, and string types.
|
|
func IntFromAny(v any) int {
|
|
switch t := v.(type) {
|
|
case int:
|
|
return t
|
|
case int32:
|
|
return int(t)
|
|
case int64:
|
|
return int(t)
|
|
case float64:
|
|
return int(t)
|
|
case string:
|
|
i, _ := strconv.Atoi(strings.TrimSpace(t))
|
|
return i
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// FloatFromAny converts a dynamic JSON value to a float64.
|
|
func FloatFromAny(v any) float64 {
|
|
switch t := v.(type) {
|
|
case float64:
|
|
return t
|
|
case int:
|
|
return float64(t)
|
|
case int64:
|
|
return float64(t)
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
// BoolFromAny converts a dynamic JSON value to a bool.
|
|
// Supports bool, string ("true"/"1"/"yes"), and numeric types.
|
|
func BoolFromAny(v any) bool {
|
|
switch t := v.(type) {
|
|
case bool:
|
|
return t
|
|
case string:
|
|
l := strings.ToLower(strings.TrimSpace(t))
|
|
return l == "1" || l == "true" || l == "yes"
|
|
case int:
|
|
return t != 0
|
|
case int64:
|
|
return t != 0
|
|
case float64:
|
|
return t != 0
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// FirstNonEmpty returns the first string in items that is non-empty after trimming.
|
|
func FirstNonEmpty(items ...string) string {
|
|
for _, item := range items {
|
|
if strings.TrimSpace(item) != "" {
|
|
return strings.TrimSpace(item)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// NestedMap returns the value at m[key] as a map[string]any.
|
|
// Returns an empty map if the key is missing or the value is not a map.
|
|
func NestedMap(m map[string]any, key string) map[string]any {
|
|
v, ok := m[key].(map[string]any)
|
|
if !ok {
|
|
return map[string]any{}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// NestedAny traverses a chain of map keys and returns the final value.
|
|
func NestedAny(v map[string]any, keys ...string) any {
|
|
cur := any(v)
|
|
for _, key := range keys {
|
|
m, ok := cur.(map[string]any)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
cur = m[key]
|
|
}
|
|
return cur
|
|
}
|
|
|
|
// NestedString traverses a chain of map keys and returns the final value as a string.
|
|
func NestedString(v map[string]any, keys ...string) string {
|
|
return StringFromAny(NestedAny(v, keys...))
|
|
}
|
|
|
|
// TitleCase capitalises the first rune of s. This is a simple ASCII replacement
|
|
// for the deprecated strings.Title function, suitable for source names like
|
|
// "qobuz" → "Qobuz".
|
|
func TitleCase(s string) string {
|
|
if s == "" {
|
|
return s
|
|
}
|
|
r := []rune(s)
|
|
if r[0] >= 'a' && r[0] <= 'z' {
|
|
r[0] -= 'a' - 'A'
|
|
}
|
|
return string(r)
|
|
}
|
|
|
|
// DisplayArtistNames joins artist names the way the providers present them:
|
|
// "A", "A & B", or "A, B & C". Previously duplicated byte-for-byte in
|
|
// internal/app, provider/yandex and provider/beatport.
|
|
func DisplayArtistNames(names []string) string {
|
|
switch len(names) {
|
|
case 0:
|
|
return ""
|
|
case 1:
|
|
return names[0]
|
|
case 2:
|
|
return names[0] + " & " + names[1]
|
|
default:
|
|
return strings.Join(names[:len(names)-1], ", ") + " & " + names[len(names)-1]
|
|
}
|
|
}
|