mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
- Extracted common JSON parsing helpers into internal/jsonutil - Removed duplicated helper functions from provider packages - Removed dead code in internal/app/app.go and downloader.go - Replaced deprecated strings.Title with jsonutil.TitleCase - Added graceful shutdown with signal handling in main.go - Split monolithic cmd/rip/main.go into args.go, helpers.go, lastfm.go, search.go
132 lines
2.9 KiB
Go
132 lines
2.9 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)
|
|
}
|