mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
Refactor: comprehensive cleanup and modularization
- 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
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"streamrip-go/internal/config"
|
||||
"streamrip-go/internal/jsonutil"
|
||||
"streamrip-go/internal/netutil"
|
||||
"streamrip-go/internal/provider"
|
||||
"streamrip-go/internal/ratelimit"
|
||||
@@ -152,7 +153,7 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
|
||||
return nil, err
|
||||
}
|
||||
enrichTrack(resp)
|
||||
if lyr, lyrErr := c.fetchLyricsFromPipe(ctx, strings.TrimSpace(stringFromAny(resp["id"]))); lyrErr == nil {
|
||||
if lyr, lyrErr := c.fetchLyricsFromPipe(ctx, strings.TrimSpace(jsonutil.StringFromAny(resp["id"]))); lyrErr == nil {
|
||||
if strings.TrimSpace(lyr.Text) != "" {
|
||||
resp["lyrics"] = lyr.Text
|
||||
}
|
||||
@@ -205,7 +206,7 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
|
||||
case "artist":
|
||||
name := strings.TrimSpace(item)
|
||||
if artistMeta, artistErr := c.apiGet(ctx, "/artist/"+item, nil); artistErr == nil {
|
||||
if n := strings.TrimSpace(stringFromAny(artistMeta["name"])); n != "" {
|
||||
if n := strings.TrimSpace(jsonutil.StringFromAny(artistMeta["name"])); n != "" {
|
||||
name = n
|
||||
}
|
||||
}
|
||||
@@ -246,7 +247,7 @@ func (c *Client) getArtistAlbums(ctx context.Context, artistID string) (map[stri
|
||||
data, _ := resp["data"].([]any)
|
||||
all = append(all, data...)
|
||||
if total < 0 {
|
||||
total = intFromAny(resp["total"])
|
||||
total = jsonutil.IntFromAny(resp["total"])
|
||||
}
|
||||
if len(data) < pageSize {
|
||||
break
|
||||
@@ -288,7 +289,7 @@ func (c *Client) GetDownloadable(ctx context.Context, item string, _ int) (*prov
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
trackToken := strings.TrimSpace(stringFromAny(meta["track_token"]))
|
||||
trackToken := strings.TrimSpace(jsonutil.StringFromAny(meta["track_token"]))
|
||||
if trackToken == "" {
|
||||
trackToken, err = c.getTrackToken(ctx, item)
|
||||
if err != nil {
|
||||
@@ -303,7 +304,7 @@ func (c *Client) GetDownloadable(ctx context.Context, item string, _ int) (*prov
|
||||
if ext == "" {
|
||||
ext = "mp3"
|
||||
}
|
||||
trackID := strings.TrimSpace(stringFromAny(meta["id"]))
|
||||
trackID := strings.TrimSpace(jsonutil.StringFromAny(meta["id"]))
|
||||
if trackID == "" {
|
||||
trackID = strings.TrimSpace(item)
|
||||
}
|
||||
@@ -344,9 +345,9 @@ func (c *Client) apiGet(ctx context.Context, path string, params url.Values) (ma
|
||||
return nil, fmt.Errorf("deezer api failed: status=%d body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
if errObj, ok := out["error"].(map[string]any); ok {
|
||||
msg := strings.TrimSpace(stringFromAny(errObj["message"]))
|
||||
msg := strings.TrimSpace(jsonutil.StringFromAny(errObj["message"]))
|
||||
if msg == "" {
|
||||
msg = strings.TrimSpace(stringFromAny(errObj["type"]))
|
||||
msg = strings.TrimSpace(jsonutil.StringFromAny(errObj["type"]))
|
||||
}
|
||||
if msg == "" {
|
||||
msg = "unknown deezer error"
|
||||
@@ -394,17 +395,17 @@ func (c *Client) refreshSessionFromARL(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
if errObj, ok := out["error"].(map[string]any); ok && len(errObj) > 0 {
|
||||
return fmt.Errorf("deezer getUserData error: %s", stringFromAny(errObj["message"]))
|
||||
return fmt.Errorf("deezer getUserData error: %s", jsonutil.StringFromAny(errObj["message"]))
|
||||
}
|
||||
results, _ := out["results"].(map[string]any)
|
||||
if len(results) == 0 {
|
||||
return errors.New("deezer getUserData returned empty results")
|
||||
}
|
||||
c.sid = firstNonEmpty(c.sid, sidFromCookies(c.http, webGWLight))
|
||||
c.sid = jsonutil.FirstNonEmpty(c.sid, sidFromCookies(c.http, webGWLight))
|
||||
c.license = findStringByKey(results, "license_token")
|
||||
c.userID = findStringByKey(results, "USER_ID")
|
||||
c.jwt = firstNonEmpty(c.jwt, findStringByKey(results, "JWT"))
|
||||
c.refresh = firstNonEmpty(c.refresh, findStringByKey(results, "refresh_token"))
|
||||
c.jwt = jsonutil.FirstNonEmpty(c.jwt, findStringByKey(results, "JWT"))
|
||||
c.refresh = jsonutil.FirstNonEmpty(c.refresh, findStringByKey(results, "refresh_token"))
|
||||
if c.sid == "" {
|
||||
if sid, sidErr := c.bootstrapSID(ctx); sidErr == nil {
|
||||
c.sid = sid
|
||||
@@ -460,7 +461,7 @@ func (c *Client) loginWithCredentials(ctx context.Context, email, password strin
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.sid = firstNonEmpty(c.sid, sid)
|
||||
c.sid = jsonutil.FirstNonEmpty(c.sid, sid)
|
||||
|
||||
encryptedPassword, err := encryptPassword(mobileToken, password)
|
||||
if err != nil {
|
||||
@@ -515,22 +516,22 @@ func (c *Client) loginWithCredentials(ctx context.Context, email, password strin
|
||||
return err
|
||||
}
|
||||
if errObj, ok := out["error"].(map[string]any); ok && len(errObj) > 0 {
|
||||
msg := firstNonEmpty(stringFromAny(errObj["message"]), stringFromAny(errObj["type"]))
|
||||
msg := jsonutil.FirstNonEmpty(jsonutil.StringFromAny(errObj["message"]), jsonutil.StringFromAny(errObj["type"]))
|
||||
if msg == "" {
|
||||
msg = "unknown mobile_userAuth error"
|
||||
}
|
||||
return errors.New(msg)
|
||||
}
|
||||
results := nestedMap(out, "results")
|
||||
results := jsonutil.NestedMap(out, "results")
|
||||
if len(results) == 0 {
|
||||
return errors.New("mobile_userAuth returned empty results")
|
||||
}
|
||||
|
||||
c.arl = firstNonEmpty(c.arl, findStringByKey(results, "ARL"))
|
||||
c.jwt = firstNonEmpty(c.jwt, findStringByKey(results, "JWT"))
|
||||
c.refresh = firstNonEmpty(c.refresh, findStringByKey(results, "refresh_token"))
|
||||
c.license = firstNonEmpty(c.license, findStringByKey(results, "license_token"))
|
||||
c.userID = firstNonEmpty(c.userID, findStringByKey(results, "USER_ID"))
|
||||
c.arl = jsonutil.FirstNonEmpty(c.arl, findStringByKey(results, "ARL"))
|
||||
c.jwt = jsonutil.FirstNonEmpty(c.jwt, findStringByKey(results, "JWT"))
|
||||
c.refresh = jsonutil.FirstNonEmpty(c.refresh, findStringByKey(results, "refresh_token"))
|
||||
c.license = jsonutil.FirstNonEmpty(c.license, findStringByKey(results, "license_token"))
|
||||
c.userID = jsonutil.FirstNonEmpty(c.userID, findStringByKey(results, "USER_ID"))
|
||||
|
||||
if c.arl == "" {
|
||||
return errors.New("mobile_userAuth missing arl")
|
||||
@@ -558,7 +559,7 @@ func (c *Client) getTrackToken(ctx context.Context, trackID string) (string, err
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
token := strings.TrimSpace(stringFromAny(resp["track_token"]))
|
||||
token := strings.TrimSpace(jsonutil.StringFromAny(resp["track_token"]))
|
||||
if token == "" {
|
||||
return "", errors.New("deezer track metadata missing track_token")
|
||||
}
|
||||
@@ -613,8 +614,8 @@ func (c *Client) fetchLyricsFromPipe(ctx context.Context, trackID string) (*lyri
|
||||
msg := ""
|
||||
typ := ""
|
||||
if em, ok := errs[0].(map[string]any); ok {
|
||||
msg = strings.TrimSpace(stringFromAny(em["message"]))
|
||||
typ = strings.TrimSpace(stringFromAny(em["type"]))
|
||||
msg = strings.TrimSpace(jsonutil.StringFromAny(em["message"]))
|
||||
typ = strings.TrimSpace(jsonutil.StringFromAny(em["type"]))
|
||||
}
|
||||
if strings.EqualFold(typ, "JwtTokenExpiredError") || strings.Contains(strings.ToLower(msg), "not valid anymore") || strings.Contains(strings.ToLower(msg), "jwt") && strings.Contains(strings.ToLower(msg), "expired") {
|
||||
return nil, errDeezerJWTExpired
|
||||
@@ -624,8 +625,8 @@ func (c *Client) fetchLyricsFromPipe(ctx context.Context, trackID string) (*lyri
|
||||
}
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
lyrics := nestedMap(nestedMap(nestedMap(out, "data"), "track"), "lyrics")
|
||||
text := strings.TrimSpace(stringFromAny(lyrics["text"]))
|
||||
lyrics := jsonutil.NestedMap(jsonutil.NestedMap(jsonutil.NestedMap(out, "data"), "track"), "lyrics")
|
||||
text := strings.TrimSpace(jsonutil.StringFromAny(lyrics["text"]))
|
||||
synced := buildSyncedLRC(lyrics["synchronizedLines"])
|
||||
if text != "" || synced != "" {
|
||||
return &lyricsResult{Text: text, SyncedLRC: synced}, nil
|
||||
@@ -637,9 +638,9 @@ func (c *Client) fetchLyricsFromPipe(ctx context.Context, trackID string) (*lyri
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
line := strings.TrimSpace(stringFromAny(m["line"]))
|
||||
line := strings.TrimSpace(jsonutil.StringFromAny(m["line"]))
|
||||
if line == "" {
|
||||
line = strings.TrimSpace(stringFromAny(m["lineTranslated"]))
|
||||
line = strings.TrimSpace(jsonutil.StringFromAny(m["lineTranslated"]))
|
||||
}
|
||||
if line != "" {
|
||||
parts = append(parts, line)
|
||||
@@ -682,14 +683,14 @@ func buildSyncedLRC(v any) string {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
line := strings.TrimSpace(stringFromAny(m["line"]))
|
||||
line := strings.TrimSpace(jsonutil.StringFromAny(m["line"]))
|
||||
if line == "" {
|
||||
line = strings.TrimSpace(stringFromAny(m["lineTranslated"]))
|
||||
line = strings.TrimSpace(jsonutil.StringFromAny(m["lineTranslated"]))
|
||||
}
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
ms := intFromAny(m["milliseconds"])
|
||||
ms := jsonutil.IntFromAny(m["milliseconds"])
|
||||
out = append(out, fmt.Sprintf("[%02d:%05.2f]%s", ms/60000, float64(ms%60000)/1000.0, line))
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
@@ -741,13 +742,13 @@ func (c *Client) mobileAuth(ctx context.Context) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
if errObj, ok := out["error"].(map[string]any); ok && len(errObj) > 0 {
|
||||
msg := firstNonEmpty(stringFromAny(errObj["message"]), stringFromAny(errObj["type"]))
|
||||
msg := jsonutil.FirstNonEmpty(jsonutil.StringFromAny(errObj["message"]), jsonutil.StringFromAny(errObj["type"]))
|
||||
if msg == "" {
|
||||
msg = "mobile_auth returned an error"
|
||||
}
|
||||
return "", errors.New(msg)
|
||||
}
|
||||
token := findStringByKey(nestedMap(out, "results"), "TOKEN")
|
||||
token := findStringByKey(jsonutil.NestedMap(out, "results"), "TOKEN")
|
||||
if token == "" {
|
||||
return "", errors.New("mobile_auth returned empty token")
|
||||
}
|
||||
@@ -788,13 +789,13 @@ func (c *Client) apiCheckToken(ctx context.Context, authToken string) (string, e
|
||||
return "", err
|
||||
}
|
||||
if errObj, ok := out["error"].(map[string]any); ok && len(errObj) > 0 {
|
||||
msg := firstNonEmpty(stringFromAny(errObj["message"]), stringFromAny(errObj["type"]))
|
||||
msg := jsonutil.FirstNonEmpty(jsonutil.StringFromAny(errObj["message"]), jsonutil.StringFromAny(errObj["type"]))
|
||||
if msg == "" {
|
||||
msg = "api_checkToken returned an error"
|
||||
}
|
||||
return "", errors.New(msg)
|
||||
}
|
||||
sid := strings.TrimSpace(stringFromAny(out["results"]))
|
||||
sid := strings.TrimSpace(jsonutil.StringFromAny(out["results"]))
|
||||
if sid == "" {
|
||||
return "", errors.New("api_checkToken returned empty sid")
|
||||
}
|
||||
@@ -852,13 +853,13 @@ func (c *Client) mobileUserAutolog(ctx context.Context) error {
|
||||
if errObj, ok := out["error"].(map[string]any); ok && len(errObj) > 0 {
|
||||
continue
|
||||
}
|
||||
results := nestedMap(out, "results")
|
||||
results := jsonutil.NestedMap(out, "results")
|
||||
if len(results) == 0 {
|
||||
continue
|
||||
}
|
||||
c.jwt = firstNonEmpty(c.jwt, findStringByKey(results, "JWT"))
|
||||
c.refresh = firstNonEmpty(c.refresh, findStringByKey(results, "refresh_token"))
|
||||
c.license = firstNonEmpty(c.license, findStringByKey(results, "license_token"))
|
||||
c.jwt = jsonutil.FirstNonEmpty(c.jwt, findStringByKey(results, "JWT"))
|
||||
c.refresh = jsonutil.FirstNonEmpty(c.refresh, findStringByKey(results, "refresh_token"))
|
||||
c.license = jsonutil.FirstNonEmpty(c.license, findStringByKey(results, "license_token"))
|
||||
if c.jwt != "" || c.license != "" {
|
||||
return nil
|
||||
}
|
||||
@@ -895,16 +896,16 @@ func (c *Client) refreshJWT(ctx context.Context) error {
|
||||
return errors.New("invalid jwt refresh response")
|
||||
}
|
||||
if errObj, ok := out["error"].(map[string]any); ok && len(errObj) > 0 {
|
||||
msg := firstNonEmpty(stringFromAny(errObj["message"]), stringFromAny(errObj["type"]))
|
||||
msg := jsonutil.FirstNonEmpty(jsonutil.StringFromAny(errObj["message"]), jsonutil.StringFromAny(errObj["type"]))
|
||||
if msg == "" {
|
||||
msg = "jwt refresh returned an error"
|
||||
}
|
||||
return errors.New(msg)
|
||||
}
|
||||
if jwt := strings.TrimSpace(stringFromAny(out["jwt"])); jwt != "" {
|
||||
if jwt := strings.TrimSpace(jsonutil.StringFromAny(out["jwt"])); jwt != "" {
|
||||
c.jwt = jwt
|
||||
}
|
||||
if rt := strings.TrimSpace(stringFromAny(out["refresh_token"])); rt != "" {
|
||||
if rt := strings.TrimSpace(jsonutil.StringFromAny(out["refresh_token"])); rt != "" {
|
||||
c.refresh = rt
|
||||
}
|
||||
if c.jwt == "" {
|
||||
@@ -951,7 +952,7 @@ func (c *Client) refreshLicenseFromPipe(ctx context.Context) error {
|
||||
if errs, ok := out["errors"].([]any); ok && len(errs) > 0 {
|
||||
msg := ""
|
||||
if em, ok := errs[0].(map[string]any); ok {
|
||||
msg = strings.TrimSpace(stringFromAny(em["message"]))
|
||||
msg = strings.TrimSpace(jsonutil.StringFromAny(em["message"]))
|
||||
}
|
||||
if msg == "" {
|
||||
msg = "pipe response returned graphql error"
|
||||
@@ -1228,7 +1229,7 @@ func findStringByKey(v any, wantedKey string) string {
|
||||
case map[string]any:
|
||||
for k, value := range x {
|
||||
if strings.ToLower(k) == w {
|
||||
if s := stringFromAny(value); strings.TrimSpace(s) != "" {
|
||||
if s := jsonutil.StringFromAny(value); strings.TrimSpace(s) != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
@@ -1246,17 +1247,9 @@ func findStringByKey(v any, wantedKey string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func enrichTrack(track map[string]any) {
|
||||
if artist, ok := track["artist"].(map[string]any); ok {
|
||||
track["performer"] = map[string]any{"name": stringFromAny(artist["name"]), "id": stringFromAny(artist["id"])}
|
||||
track["performer"] = map[string]any{"name": jsonutil.StringFromAny(artist["name"]), "id": jsonutil.StringFromAny(artist["id"])}
|
||||
}
|
||||
if album, ok := track["album"].(map[string]any); ok {
|
||||
enrichAlbumImage(album)
|
||||
@@ -1271,7 +1264,7 @@ func enrichTrack(track map[string]any) {
|
||||
track["media_number"] = d
|
||||
}
|
||||
}
|
||||
if boolFromAny(track["explicit_lyrics"]) {
|
||||
if jsonutil.BoolFromAny(track["explicit_lyrics"]) {
|
||||
track["explicit"] = true
|
||||
}
|
||||
}
|
||||
@@ -1280,11 +1273,11 @@ func enrichAlbumImage(meta map[string]any) {
|
||||
if _, ok := meta["image"].(map[string]any); ok {
|
||||
return
|
||||
}
|
||||
cover := firstNonEmpty(
|
||||
stringFromAny(meta["cover_xl"]),
|
||||
stringFromAny(meta["cover_big"]),
|
||||
stringFromAny(meta["cover_medium"]),
|
||||
stringFromAny(meta["cover_small"]),
|
||||
cover := jsonutil.FirstNonEmpty(
|
||||
jsonutil.StringFromAny(meta["cover_xl"]),
|
||||
jsonutil.StringFromAny(meta["cover_big"]),
|
||||
jsonutil.StringFromAny(meta["cover_medium"]),
|
||||
jsonutil.StringFromAny(meta["cover_small"]),
|
||||
)
|
||||
if cover == "" {
|
||||
return
|
||||
@@ -1296,48 +1289,3 @@ func enrichAlbumImage(meta map[string]any) {
|
||||
"original": cover,
|
||||
}
|
||||
}
|
||||
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmpty(items ...string) string {
|
||||
for _, item := range items {
|
||||
if strings.TrimSpace(item) != "" {
|
||||
return strings.TrimSpace(item)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func intFromAny(v any) int {
|
||||
switch t := v.(type) {
|
||||
case int:
|
||||
return t
|
||||
case int64:
|
||||
return int(t)
|
||||
case float64:
|
||||
return int(t)
|
||||
case string:
|
||||
i, _ := strconv.Atoi(strings.TrimSpace(t))
|
||||
return i
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func boolFromAny(v any) bool {
|
||||
b, ok := v.(bool)
|
||||
return ok && b
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"streamrip-go/internal/jsonutil"
|
||||
|
||||
"streamrip-go/internal/config"
|
||||
)
|
||||
|
||||
@@ -89,7 +91,7 @@ func TestGetMetadataArtistPaginatesAlbums(t *testing.T) {
|
||||
if len(items) != 101 {
|
||||
t.Fatalf("albums len = %d, want 101", len(items))
|
||||
}
|
||||
if got := strings.TrimSpace(stringFromAny(meta["name"])); got != "Lost Frequencies" {
|
||||
if got := strings.TrimSpace(jsonutil.StringFromAny(meta["name"])); got != "Lost Frequencies" {
|
||||
t.Fatalf("artist name = %q, want Lost Frequencies", got)
|
||||
}
|
||||
if callCount != 2 {
|
||||
@@ -220,11 +222,11 @@ func TestGetMetadataAddsLyricsFromPipe(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetMetadata() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(stringFromAny(meta["lyrics"]), "Go shawty") {
|
||||
t.Fatalf("expected lyrics text, got %q", stringFromAny(meta["lyrics"]))
|
||||
if !strings.Contains(jsonutil.StringFromAny(meta["lyrics"]), "Go shawty") {
|
||||
t.Fatalf("expected lyrics text, got %q", jsonutil.StringFromAny(meta["lyrics"]))
|
||||
}
|
||||
if !strings.Contains(stringFromAny(meta["lyrics_synced"]), "[00:00.00]Go, go, go") {
|
||||
t.Fatalf("expected synced lyrics, got %q", stringFromAny(meta["lyrics_synced"]))
|
||||
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"]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,7 +245,7 @@ func TestLoginWithCredentials(t *testing.T) {
|
||||
case "mobile_userAuth":
|
||||
var payload map[string]any
|
||||
_ = json.NewDecoder(r.Body).Decode(&payload)
|
||||
if strings.TrimSpace(stringFromAny(payload["mail"])) == "" || strings.TrimSpace(stringFromAny(payload["password"])) == "" {
|
||||
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
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
"streamrip-go/internal/config"
|
||||
"streamrip-go/internal/jsonutil"
|
||||
"streamrip-go/internal/netutil"
|
||||
"streamrip-go/internal/provider"
|
||||
"streamrip-go/internal/ratelimit"
|
||||
@@ -694,7 +695,7 @@ func (c *Client) fetchAppIDAndSecrets(ctx context.Context) (string, []string, er
|
||||
|
||||
tzNames := make([]string, 0, len(ordered))
|
||||
for _, o := range ordered {
|
||||
tzNames = append(tzNames, strings.Title(o.timezone))
|
||||
tzNames = append(tzNames, jsonutil.TitleCase(o.timezone))
|
||||
}
|
||||
infoRe := regexp.MustCompile(fmt.Sprintf(infoExtrasTemplate, strings.Join(tzNames, "|")))
|
||||
idxInfo := infoRe.SubexpIndex("info")
|
||||
|
||||
@@ -10,12 +10,12 @@ import (
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"streamrip-go/internal/config"
|
||||
"streamrip-go/internal/jsonutil"
|
||||
"streamrip-go/internal/provider"
|
||||
)
|
||||
|
||||
@@ -102,14 +102,14 @@ func (c *Client) searchTracks(ctx context.Context, query string, limit int) ([]m
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
artist := strings.TrimSpace(stringFromAny(m["uploader"]))
|
||||
artist := strings.TrimSpace(jsonutil.StringFromAny(m["uploader"]))
|
||||
if artist == "" {
|
||||
artist = strings.TrimSpace(stringFromAny(m["channel"]))
|
||||
artist = strings.TrimSpace(jsonutil.StringFromAny(m["channel"]))
|
||||
}
|
||||
artistID := strings.TrimSpace(firstNonEmpty(stringFromAny(m["uploader_id"]), stringFromAny(m["channel_id"])))
|
||||
artistID := strings.TrimSpace(jsonutil.FirstNonEmpty(jsonutil.StringFromAny(m["uploader_id"]), jsonutil.StringFromAny(m["channel_id"])))
|
||||
item := map[string]any{
|
||||
"id": id,
|
||||
"title": stringFromAny(m["title"]),
|
||||
"title": jsonutil.StringFromAny(m["title"]),
|
||||
"artist": map[string]any{
|
||||
"name": artist,
|
||||
},
|
||||
@@ -117,7 +117,7 @@ func (c *Client) searchTracks(ctx context.Context, query string, limit int) ([]m
|
||||
if artistID != "" {
|
||||
item["artist"] = map[string]any{"name": artist, "id": artistID}
|
||||
}
|
||||
if trackID := strings.TrimSpace(stringFromAny(m["id"])); trackID != "" {
|
||||
if trackID := strings.TrimSpace(jsonutil.StringFromAny(m["id"])); trackID != "" {
|
||||
item["source_track_id"] = trackID
|
||||
}
|
||||
items = append(items, item)
|
||||
@@ -163,17 +163,17 @@ func (c *Client) searchPlaylists(ctx context.Context, query string, limit int) (
|
||||
if infoErr != nil {
|
||||
continue
|
||||
}
|
||||
title := strings.TrimSpace(stringFromAny(info["title"]))
|
||||
title := strings.TrimSpace(jsonutil.StringFromAny(info["title"]))
|
||||
if title == "" {
|
||||
title = strings.Trim(strings.ReplaceAll(path, "/", " "), " ")
|
||||
}
|
||||
artist := strings.TrimSpace(firstNonEmpty(stringFromAny(info["uploader"]), stringFromAny(info["channel"])))
|
||||
artistID := strings.TrimSpace(firstNonEmpty(stringFromAny(info["uploader_id"]), stringFromAny(info["channel_id"])))
|
||||
artist := strings.TrimSpace(jsonutil.FirstNonEmpty(jsonutil.StringFromAny(info["uploader"]), jsonutil.StringFromAny(info["channel"])))
|
||||
artistID := strings.TrimSpace(jsonutil.FirstNonEmpty(jsonutil.StringFromAny(info["uploader_id"]), jsonutil.StringFromAny(info["channel_id"])))
|
||||
trackCount := 0
|
||||
if entries := asAnySlice(info["entries"]); len(entries) > 0 {
|
||||
trackCount = len(entries)
|
||||
}
|
||||
canonical := firstNonEmpty(canonicalSoundcloudURL(info), playlistURL)
|
||||
canonical := jsonutil.FirstNonEmpty(canonicalSoundcloudURL(info), playlistURL)
|
||||
item := map[string]any{
|
||||
"id": canonical,
|
||||
"title": title,
|
||||
@@ -183,10 +183,10 @@ func (c *Client) searchPlaylists(ctx context.Context, query string, limit int) (
|
||||
if artistID != "" {
|
||||
item["artist"] = map[string]any{"name": artist, "id": artistID}
|
||||
}
|
||||
if pid := strings.TrimSpace(stringFromAny(info["id"])); pid != "" {
|
||||
if pid := strings.TrimSpace(jsonutil.StringFromAny(info["id"])); pid != "" {
|
||||
item["source_playlist_id"] = pid
|
||||
}
|
||||
if thumb := strings.TrimSpace(stringFromAny(info["thumbnail"])); thumb != "" {
|
||||
if thumb := strings.TrimSpace(jsonutil.StringFromAny(info["thumbnail"])); thumb != "" {
|
||||
item["image"] = soundcloudImageMap(thumb)
|
||||
}
|
||||
items = append(items, item)
|
||||
@@ -228,15 +228,15 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
|
||||
continue
|
||||
}
|
||||
track := map[string]any{"id": id}
|
||||
if trackID := strings.TrimSpace(stringFromAny(entry["id"])); trackID != "" {
|
||||
if trackID := strings.TrimSpace(jsonutil.StringFromAny(entry["id"])); trackID != "" {
|
||||
track["source_track_id"] = trackID
|
||||
}
|
||||
if title := strings.TrimSpace(stringFromAny(entry["title"])); title != "" {
|
||||
if title := strings.TrimSpace(jsonutil.StringFromAny(entry["title"])); title != "" {
|
||||
track["title"] = title
|
||||
}
|
||||
if artist := strings.TrimSpace(firstNonEmpty(stringFromAny(entry["uploader"]), stringFromAny(entry["channel"]))); artist != "" {
|
||||
if artist := strings.TrimSpace(jsonutil.FirstNonEmpty(jsonutil.StringFromAny(entry["uploader"]), jsonutil.StringFromAny(entry["channel"]))); artist != "" {
|
||||
artistMap := map[string]any{"name": artist}
|
||||
if artistID := strings.TrimSpace(firstNonEmpty(stringFromAny(entry["uploader_id"]), stringFromAny(entry["channel_id"]))); artistID != "" {
|
||||
if artistID := strings.TrimSpace(jsonutil.FirstNonEmpty(jsonutil.StringFromAny(entry["uploader_id"]), jsonutil.StringFromAny(entry["channel_id"]))); artistID != "" {
|
||||
artistMap["id"] = artistID
|
||||
}
|
||||
track["artist"] = artistMap
|
||||
@@ -244,23 +244,23 @@ func (c *Client) GetMetadata(ctx context.Context, item, mediaType string) (map[s
|
||||
track["track_number"] = i + 1
|
||||
tracks = append(tracks, track)
|
||||
}
|
||||
name := strings.TrimSpace(stringFromAny(root["title"]))
|
||||
name := strings.TrimSpace(jsonutil.StringFromAny(root["title"]))
|
||||
if name == "" {
|
||||
name = "SoundCloud Playlist"
|
||||
}
|
||||
meta := map[string]any{
|
||||
"id": firstNonEmpty(canonicalSoundcloudURL(root), item),
|
||||
"id": jsonutil.FirstNonEmpty(canonicalSoundcloudURL(root), item),
|
||||
"name": name,
|
||||
"description": strings.TrimSpace(stringFromAny(root["description"])),
|
||||
"description": strings.TrimSpace(jsonutil.StringFromAny(root["description"])),
|
||||
"tracks": map[string]any{"items": tracks},
|
||||
}
|
||||
if pid := strings.TrimSpace(stringFromAny(root["id"])); pid != "" {
|
||||
if pid := strings.TrimSpace(jsonutil.StringFromAny(root["id"])); pid != "" {
|
||||
meta["source_playlist_id"] = pid
|
||||
}
|
||||
if artist := strings.TrimSpace(firstNonEmpty(stringFromAny(root["uploader"]), stringFromAny(root["channel"]))); artist != "" {
|
||||
if artist := strings.TrimSpace(jsonutil.FirstNonEmpty(jsonutil.StringFromAny(root["uploader"]), jsonutil.StringFromAny(root["channel"]))); artist != "" {
|
||||
meta["artist"] = map[string]any{"name": artist}
|
||||
}
|
||||
if thumb := strings.TrimSpace(stringFromAny(root["thumbnail"])); thumb != "" {
|
||||
if thumb := strings.TrimSpace(jsonutil.StringFromAny(root["thumbnail"])); thumb != "" {
|
||||
meta["image"] = soundcloudImageMap(thumb)
|
||||
}
|
||||
if entries := asAnySlice(root["entries"]); len(entries) > 0 {
|
||||
@@ -280,11 +280,11 @@ func (c *Client) GetDownloadable(ctx context.Context, item string, _ int) (*prov
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
streamURL := strings.TrimSpace(stringFromAny(info["url"]))
|
||||
streamURL := strings.TrimSpace(jsonutil.StringFromAny(info["url"]))
|
||||
if streamURL == "" {
|
||||
return nil, errors.New("yt-dlp output missing url (track may be unavailable or region-restricted)")
|
||||
}
|
||||
ext := strings.TrimSpace(stringFromAny(info["ext"]))
|
||||
ext := strings.TrimSpace(jsonutil.StringFromAny(info["ext"]))
|
||||
if ext == "" {
|
||||
ext = "m4a"
|
||||
}
|
||||
@@ -337,36 +337,36 @@ func (c *Client) playlistInfo(ctx context.Context, item string) (map[string]any,
|
||||
}
|
||||
|
||||
func trackMetadataFromInfo(id string, info map[string]any) map[string]any {
|
||||
canonicalID := firstNonEmpty(canonicalSoundcloudURL(info), id)
|
||||
publisher := nestedMap(info, "publisher_metadata")
|
||||
title := strings.TrimSpace(stringFromAny(info["title"]))
|
||||
canonicalID := jsonutil.FirstNonEmpty(canonicalSoundcloudURL(info), id)
|
||||
publisher := jsonutil.NestedMap(info, "publisher_metadata")
|
||||
title := strings.TrimSpace(jsonutil.StringFromAny(info["title"]))
|
||||
if title == "" {
|
||||
title = canonicalID
|
||||
}
|
||||
albumTitle := strings.TrimSpace(stringFromAny(publisher["album_title"]))
|
||||
albumTitle := strings.TrimSpace(jsonutil.StringFromAny(publisher["album_title"]))
|
||||
if albumTitle == "" {
|
||||
albumTitle = strings.TrimSpace(stringFromAny(info["album"]))
|
||||
albumTitle = strings.TrimSpace(jsonutil.StringFromAny(info["album"]))
|
||||
}
|
||||
if albumTitle == "" {
|
||||
albumTitle = title
|
||||
}
|
||||
artistName := strings.TrimSpace(stringFromAny(info["artist"]))
|
||||
artistName := strings.TrimSpace(jsonutil.StringFromAny(info["artist"]))
|
||||
if artistName == "" {
|
||||
artistName = strings.TrimSpace(stringFromAny(publisher["artist"]))
|
||||
artistName = strings.TrimSpace(jsonutil.StringFromAny(publisher["artist"]))
|
||||
}
|
||||
if artistName == "" {
|
||||
artistName = strings.TrimSpace(stringFromAny(info["uploader"]))
|
||||
artistName = strings.TrimSpace(jsonutil.StringFromAny(info["uploader"]))
|
||||
}
|
||||
if artistName == "" {
|
||||
artistName = strings.TrimSpace(stringFromAny(info["channel"]))
|
||||
artistName = strings.TrimSpace(jsonutil.StringFromAny(info["channel"]))
|
||||
}
|
||||
artistID := strings.TrimSpace(firstNonEmpty(
|
||||
stringFromAny(info["uploader_id"]),
|
||||
stringFromAny(info["channel_id"]),
|
||||
stringFromAny(nestedMap(info, "user")["id"]),
|
||||
artistID := strings.TrimSpace(jsonutil.FirstNonEmpty(
|
||||
jsonutil.StringFromAny(info["uploader_id"]),
|
||||
jsonutil.StringFromAny(info["channel_id"]),
|
||||
jsonutil.StringFromAny(jsonutil.NestedMap(info, "user")["id"]),
|
||||
))
|
||||
|
||||
trackNum := intFromAny(info["track_number"])
|
||||
trackNum := jsonutil.IntFromAny(info["track_number"])
|
||||
if trackNum <= 0 {
|
||||
trackNum = 1
|
||||
}
|
||||
@@ -378,26 +378,26 @@ func trackMetadataFromInfo(id string, info map[string]any) map[string]any {
|
||||
"artist": map[string]any{"name": artistName, "id": artistID},
|
||||
"performer": map[string]any{"name": artistName, "id": artistID},
|
||||
"album": map[string]any{
|
||||
"id": firstNonEmpty(strings.TrimSpace(stringFromAny(info["album"])), canonicalID),
|
||||
"id": jsonutil.FirstNonEmpty(strings.TrimSpace(jsonutil.StringFromAny(info["album"])), canonicalID),
|
||||
"title": albumTitle,
|
||||
"artist": map[string]any{"name": artistName, "id": artistID},
|
||||
},
|
||||
"description": strings.TrimSpace(stringFromAny(info["description"])),
|
||||
"genre": strings.TrimSpace(stringFromAny(info["genre"])),
|
||||
"isrc": strings.TrimSpace(stringFromAny(info["isrc"])),
|
||||
"label": strings.TrimSpace(firstNonEmpty(stringFromAny(info["label"]), stringFromAny(info["label_name"]))),
|
||||
"copyright": strings.TrimSpace(stringFromAny(publisher["p_line"])),
|
||||
"release_date": strings.TrimSpace(firstNonEmpty(
|
||||
stringFromAny(info["created_at"]),
|
||||
stringFromAny(info["release_date"]),
|
||||
stringFromAny(info["upload_date"]),
|
||||
"description": strings.TrimSpace(jsonutil.StringFromAny(info["description"])),
|
||||
"genre": strings.TrimSpace(jsonutil.StringFromAny(info["genre"])),
|
||||
"isrc": strings.TrimSpace(jsonutil.StringFromAny(info["isrc"])),
|
||||
"label": strings.TrimSpace(jsonutil.FirstNonEmpty(jsonutil.StringFromAny(info["label"]), jsonutil.StringFromAny(info["label_name"]))),
|
||||
"copyright": strings.TrimSpace(jsonutil.StringFromAny(publisher["p_line"])),
|
||||
"release_date": strings.TrimSpace(jsonutil.FirstNonEmpty(
|
||||
jsonutil.StringFromAny(info["created_at"]),
|
||||
jsonutil.StringFromAny(info["release_date"]),
|
||||
jsonutil.StringFromAny(info["upload_date"]),
|
||||
)),
|
||||
}
|
||||
if trackID := strings.TrimSpace(stringFromAny(info["id"])); trackID != "" {
|
||||
if trackID := strings.TrimSpace(jsonutil.StringFromAny(info["id"])); trackID != "" {
|
||||
meta["source_track_id"] = trackID
|
||||
}
|
||||
|
||||
if boolFromAny(publisher["explicit"]) || intFromAny(info["age_limit"]) >= 18 {
|
||||
if jsonutil.BoolFromAny(publisher["explicit"]) || jsonutil.IntFromAny(info["age_limit"]) >= 18 {
|
||||
meta["explicit"] = true
|
||||
}
|
||||
|
||||
@@ -405,11 +405,11 @@ func trackMetadataFromInfo(id string, info map[string]any) map[string]any {
|
||||
delete(meta, "release_date")
|
||||
}
|
||||
|
||||
if thumb := strings.TrimSpace(stringFromAny(info["thumbnail"])); thumb != "" {
|
||||
if thumb := strings.TrimSpace(jsonutil.StringFromAny(info["thumbnail"])); thumb != "" {
|
||||
meta["image"] = soundcloudImageMap(thumb)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(stringFromAny(info["album"])) == "" && strings.TrimSpace(stringFromAny(publisher["album_title"])) == "" {
|
||||
if strings.TrimSpace(jsonutil.StringFromAny(info["album"])) == "" && strings.TrimSpace(jsonutil.StringFromAny(publisher["album_title"])) == "" {
|
||||
meta["album"] = map[string]any{
|
||||
"id": canonicalID,
|
||||
"title": title,
|
||||
@@ -417,7 +417,7 @@ func trackMetadataFromInfo(id string, info map[string]any) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
if durationSec := intFromAny(info["duration"]); durationSec > 0 {
|
||||
if durationSec := jsonutil.IntFromAny(info["duration"]); durationSec > 0 {
|
||||
meta["duration"] = durationSec
|
||||
}
|
||||
|
||||
@@ -426,7 +426,7 @@ func trackMetadataFromInfo(id string, info map[string]any) map[string]any {
|
||||
|
||||
func canonicalSoundcloudURL(info map[string]any) string {
|
||||
for _, key := range []string{"webpage_url", "original_url", "url"} {
|
||||
raw := strings.TrimSpace(stringFromAny(info[key]))
|
||||
raw := strings.TrimSpace(jsonutil.StringFromAny(info[key]))
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
@@ -478,72 +478,6 @@ func asAnySlice(v any) []any {
|
||||
return items
|
||||
}
|
||||
|
||||
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 ""
|
||||
}
|
||||
}
|
||||
|
||||
func intFromAny(v any) int {
|
||||
switch t := v.(type) {
|
||||
case int:
|
||||
return t
|
||||
case int64:
|
||||
return int(t)
|
||||
case float64:
|
||||
return int(t)
|
||||
case string:
|
||||
i, _ := strconv.Atoi(strings.TrimSpace(t))
|
||||
return i
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmpty(items ...string) string {
|
||||
for _, item := range items {
|
||||
if strings.TrimSpace(item) != "" {
|
||||
return strings.TrimSpace(item)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func soundcloudImageMap(raw string) map[string]any {
|
||||
base := strings.TrimSpace(raw)
|
||||
if base == "" {
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"streamrip-go/internal/jsonutil"
|
||||
|
||||
"streamrip-go/internal/config"
|
||||
)
|
||||
|
||||
@@ -27,11 +29,11 @@ func TestGetTrackMetadataAndDownloadable(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetMetadata() error = %v", err)
|
||||
}
|
||||
if stringFromAny(meta["title"]) != "Lean On" {
|
||||
t.Fatalf("title = %q, want Lean On", stringFromAny(meta["title"]))
|
||||
if jsonutil.StringFromAny(meta["title"]) != "Lean On" {
|
||||
t.Fatalf("title = %q, want Lean On", jsonutil.StringFromAny(meta["title"]))
|
||||
}
|
||||
if stringFromAny(meta["id"]) != "https://soundcloud.com/a/b" {
|
||||
t.Fatalf("id = %q, want canonical soundcloud url", stringFromAny(meta["id"]))
|
||||
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)
|
||||
@@ -59,8 +61,8 @@ func TestGetPlaylistMetadata(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetMetadata() error = %v", err)
|
||||
}
|
||||
if stringFromAny(meta["name"]) != "Road Trip" {
|
||||
t.Fatalf("name = %q, want Road Trip", stringFromAny(meta["name"]))
|
||||
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 {
|
||||
@@ -70,8 +72,8 @@ func TestGetPlaylistMetadata(t *testing.T) {
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("playlist items len = %d, want 2", len(items))
|
||||
}
|
||||
if stringFromAny(meta["id"]) != "https://soundcloud.com/a/sets/road-trip" {
|
||||
t.Fatalf("playlist id not canonical: %q", stringFromAny(meta["id"]))
|
||||
if jsonutil.StringFromAny(meta["id"]) != "https://soundcloud.com/a/sets/road-trip" {
|
||||
t.Fatalf("playlist id not canonical: %q", jsonutil.StringFromAny(meta["id"]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,8 +104,8 @@ func TestSearchTrack(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("expected first item map")
|
||||
}
|
||||
if stringFromAny(item0["id"]) != "https://soundcloud.com/a/b" {
|
||||
t.Fatalf("track search id not canonical: %q", stringFromAny(item0["id"]))
|
||||
if jsonutil.StringFromAny(item0["id"]) != "https://soundcloud.com/a/b" {
|
||||
t.Fatalf("track search id not canonical: %q", jsonutil.StringFromAny(item0["id"]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,8 +149,8 @@ func TestSearchPlaylist(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("expected first item map")
|
||||
}
|
||||
if stringFromAny(item0["id"]) != "https://soundcloud.com/a/sets/road-trip" {
|
||||
t.Fatalf("playlist search id not canonical: %q", stringFromAny(item0["id"]))
|
||||
if jsonutil.StringFromAny(item0["id"]) != "https://soundcloud.com/a/sets/road-trip" {
|
||||
t.Fatalf("playlist search id not canonical: %q", jsonutil.StringFromAny(item0["id"]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,8 +194,8 @@ func TestSearchPlaylistAcceptsDotsInPath(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("expected first item map")
|
||||
}
|
||||
if stringFromAny(item0["id"]) != "https://soundcloud.com/artist.name/sets/road.trip" {
|
||||
t.Fatalf("playlist search id not canonical: %q", stringFromAny(item0["id"]))
|
||||
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"]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,18 +223,18 @@ func TestTrackMetadataIncludesExplicitAndISRC(t *testing.T) {
|
||||
"thumbnail": "https://img",
|
||||
"upload_date": "20240101",
|
||||
})
|
||||
if stringFromAny(meta["isrc"]) != "US123" {
|
||||
t.Fatalf("isrc = %q, want US123", stringFromAny(meta["isrc"]))
|
||||
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 stringFromAny(meta["source_track_id"]) != "9876" {
|
||||
t.Fatalf("source_track_id = %q, want 9876", stringFromAny(meta["source_track_id"]))
|
||||
if jsonutil.StringFromAny(meta["source_track_id"]) != "9876" {
|
||||
t.Fatalf("source_track_id = %q, want 9876", jsonutil.StringFromAny(meta["source_track_id"]))
|
||||
}
|
||||
if stringFromAny(nestedMap(meta, "album")["title"]) != "T" {
|
||||
t.Fatalf("album title mismatch: %#v", nestedMap(meta, "album"))
|
||||
if jsonutil.StringFromAny(jsonutil.NestedMap(meta, "album")["title"]) != "T" {
|
||||
t.Fatalf("album title mismatch: %#v", jsonutil.NestedMap(meta, "album"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"streamrip-go/internal/config"
|
||||
"streamrip-go/internal/jsonutil"
|
||||
"streamrip-go/internal/netutil"
|
||||
"streamrip-go/internal/provider"
|
||||
"streamrip-go/internal/ratelimit"
|
||||
@@ -149,7 +150,7 @@ func (c *Client) refreshAccessToken(ctx context.Context) error {
|
||||
}
|
||||
|
||||
newRefresh := stringify(resp["refresh_token"])
|
||||
expiresIn := int64(intFromAny(resp["expires_in"]))
|
||||
expiresIn := int64(jsonutil.IntFromAny(resp["expires_in"]))
|
||||
if expiresIn <= 0 {
|
||||
expiresIn = 7 * 24 * 3600
|
||||
}
|
||||
@@ -773,19 +774,3 @@ func tidalImageMap(cover string) map[string]any {
|
||||
"original": base + "/1280x1280.jpg",
|
||||
}
|
||||
}
|
||||
|
||||
func intFromAny(v any) int {
|
||||
switch t := v.(type) {
|
||||
case int:
|
||||
return t
|
||||
case int64:
|
||||
return int(t)
|
||||
case float64:
|
||||
return int(t)
|
||||
case string:
|
||||
i, _ := strconv.Atoi(t)
|
||||
return i
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user