Something went wrong. Try again.
a simple (and fast) soundcloud downloader
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262package downcloud
import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strings" "time")
type APIError struct { StatusCode int Message string}
func (e *APIError) Error() string { return fmt.Sprintf("API error: %d", e.StatusCode)}
func isAuthError(err error) bool { var apiErr *APIError if errors.As(err, &apiErr) { return apiErr.StatusCode == http.StatusUnauthorized || apiErr.StatusCode == http.StatusForbidden } return false}
func doResolveURL(ctx context.Context, targetURL, clientID, oauthToken string) ([]byte, error) { reqURL := fmt.Sprintf("https://api-v2.soundcloud.com/resolve?url=%s&client_id=%s", url.QueryEscape(targetURL), clientID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", UserAgent) if oauthToken != "" { req.Header.Set("Authorization", "OAuth "+oauthToken) }
client := &http.Client{Timeout: 30 * time.Second} res, err := client.Do(req) if err != nil { return nil, err } defer res.Body.Close()
if res.StatusCode != http.StatusOK { return nil, &APIError{StatusCode: res.StatusCode} }
return io.ReadAll(res.Body)}
func ResolveURL(ctx context.Context, targetURL, clientID, oauthToken string) ([]byte, error) { data, err := doResolveURL(ctx, targetURL, clientID, oauthToken) if err != nil && isAuthError(err) { if newCID, refErr := RefreshClientID(); refErr == nil && newCID != "" { return doResolveURL(ctx, targetURL, newCID, oauthToken) } } return data, err}
func doFetchFullTrack(ctx context.Context, trackID int64, clientID, oauthToken string) (*Track, error) { reqURL := fmt.Sprintf("https://api-v2.soundcloud.com/tracks/%d?client_id=%s", trackID, clientID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", UserAgent) if oauthToken != "" { req.Header.Set("Authorization", "OAuth "+oauthToken) }
client := &http.Client{Timeout: 30 * time.Second} res, err := client.Do(req) if err != nil { return nil, err } defer res.Body.Close()
if res.StatusCode != http.StatusOK { return nil, &APIError{StatusCode: res.StatusCode} }
var track Track if err := json.NewDecoder(res.Body).Decode(&track); err != nil { return nil, err } return &track, nil}
func FetchFullTrack(ctx context.Context, trackID int64, clientID, oauthToken string) (*Track, error) { track, err := doFetchFullTrack(ctx, trackID, clientID, oauthToken) if err != nil && isAuthError(err) { if newCID, refErr := RefreshClientID(); refErr == nil && newCID != "" { return doFetchFullTrack(ctx, trackID, newCID, oauthToken) } } return track, err}
type trackCollectionResponse struct { Collection []Track `json:"collection"` NextHref *string `json:"next_href"`}
func FetchArtistTracks(ctx context.Context, artistURL, clientID, oauthToken string) (*ArtistData, error) { raw, err := ResolveURL(ctx, artistURL, clientID, oauthToken) if err != nil { return nil, err }
var user User if err := json.Unmarshal(raw, &user); err != nil { return nil, fmt.Errorf("failed to parse user info: %w", err) }
tracks, err := FetchUserTracks(ctx, user.ID, clientID, oauthToken) if err != nil { return nil, err }
return &ArtistData{ User: user, Tracks: tracks, }, nil}
func FetchUserTracks(ctx context.Context, userID int64, clientID, oauthToken string) ([]Track, error) { var tracks []Track nextURL := fmt.Sprintf("https://api-v2.soundcloud.com/users/%d/tracks?client_id=%s&limit=50", userID, clientID)
client := &http.Client{Timeout: 30 * time.Second} currentCID := clientID
for nextURL != "" { req, err := http.NewRequestWithContext(ctx, http.MethodGet, nextURL, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", UserAgent) if oauthToken != "" { req.Header.Set("Authorization", "OAuth "+oauthToken) }
res, err := client.Do(req) if err != nil { return nil, err }
if res.StatusCode == http.StatusUnauthorized || res.StatusCode == http.StatusForbidden { res.Body.Close() if newCID, refErr := RefreshClientID(); refErr == nil && newCID != "" { currentCID = newCID parsed, parseErr := url.Parse(nextURL) if parseErr == nil { q := parsed.Query() q.Set("client_id", currentCID) parsed.RawQuery = q.Encode() nextURL = parsed.String() }
req, err = http.NewRequestWithContext(ctx, http.MethodGet, nextURL, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", UserAgent) if oauthToken != "" { req.Header.Set("Authorization", "OAuth "+oauthToken) } res, err = client.Do(req) if err != nil { return nil, err } } }
if res.StatusCode != http.StatusOK { res.Body.Close() return nil, &APIError{StatusCode: res.StatusCode} }
var data trackCollectionResponse err = json.NewDecoder(res.Body).Decode(&data) res.Body.Close() if err != nil { return nil, err }
tracks = append(tracks, data.Collection...)
if data.NextHref != nil && *data.NextHref != "" { parsed, err := url.Parse(*data.NextHref) if err != nil { break } q := parsed.Query() if q.Get("client_id") == "" { q.Set("client_id", currentCID) parsed.RawQuery = q.Encode() } nextURL = parsed.String() } else { nextURL = "" }
if len(data.Collection) == 0 { break } }
return tracks, nil}
type ResolvedKind string
const ( KindTrack ResolvedKind = "track" KindPlaylist ResolvedKind = "playlist" KindArtist ResolvedKind = "artist" KindUnknown ResolvedKind = "unknown")
func DetectKind(raw []byte) ResolvedKind { var m map[string]json.RawMessage if err := json.Unmarshal(raw, &m); err != nil { return KindUnknown }
if _, ok := m["media"]; ok { return KindTrack } if _, ok := m["tracks"]; ok { return KindPlaylist } if _, ok := m["username"]; ok { if _, hasPermalink := m["permalink"]; hasPermalink { return KindArtist } }
var kindStruct struct { Kind string `json:"kind"` } if err := json.Unmarshal(raw, &kindStruct); err == nil { switch strings.ToLower(kindStruct.Kind) { case "track": return KindTrack case "playlist": return KindPlaylist case "user": return KindArtist } }
return KindUnknown}