package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "regexp" "strings" "github.com/PuerkitoBio/goquery" ) type SoundCloudTrack struct { ID int `json:"id"` Title string `json:"title"` Permalink string `json:"permalink"` ArtworkURL string `json:"artwork_url"` Duration int `json:"duration"` User struct { Username string `json:"username"` Permalink string `json:"permalink"` } `json:"user"` Streams struct { HTTPMP3128URL string `json:"http_mp3_128_url"` } `json:"streams"` } func SearchSoundCloud(query string, page int) ([]MusicResult, error) { clientID, err := extractClientID() if err != nil { return searchSoundCloudViaScraping(query, page) } apiResults, err := searchSoundCloudViaAPI(query, clientID, page) if err == nil && len(apiResults) > 0 { return convertSoundCloudResults(apiResults), nil } return searchSoundCloudViaScraping(query, page) } func searchSoundCloudViaAPI(query, clientID string, page int) ([]SoundCloudTrack, error) { const limit = 10 offset := (page - 1) * limit apiUrl := fmt.Sprintf( "https://api-v2.soundcloud.com/search/tracks?q=%s&client_id=%s&limit=%d&offset=%d", url.QueryEscape(query), clientID, limit, offset, ) resp, err := http.Get(apiUrl) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API request failed with status: %d", resp.StatusCode) } var response struct { Collection []SoundCloudTrack `json:"collection"` } if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { return nil, err } return response.Collection, nil } func convertSoundCloudResults(tracks []SoundCloudTrack) []MusicResult { var results []MusicResult for _, track := range tracks { thumbnail := strings.Replace(track.ArtworkURL, "large", "t500x500", 1) trackURL := fmt.Sprintf("https://soundcloud.com/%s/%s", track.User.Permalink, track.Permalink, ) results = append(results, MusicResult{ Title: track.Title, Artist: track.User.Username, URL: trackURL, Thumbnail: thumbnail, //AudioURL: track.Streams.HTTPMP3128URL, Source: "SoundCloud", Duration: fmt.Sprintf("%d", track.Duration/1000), }) } return results } func searchSoundCloudViaScraping(query string, page int) ([]MusicResult, error) { searchUrl := fmt.Sprintf("https://soundcloud.com/search/sounds?q=%s", url.QueryEscape(query)) resp, err := http.Get(searchUrl) if err != nil { return nil, err } defer resp.Body.Close() doc, err := goquery.NewDocumentFromReader(resp.Body) if err != nil { return nil, err } var results []MusicResult doc.Find("li.searchList__item").Each(func(i int, s *goquery.Selection) { titleElem := s.Find("a.soundTitle__title") artistElem := s.Find("a.soundTitle__username") artworkElem := s.Find(".sound__coverArt") title := strings.TrimSpace(titleElem.Text()) artist := strings.TrimSpace(artistElem.Text()) href, _ := titleElem.Attr("href") thumbnail, _ := artworkElem.Find("span.sc-artwork").Attr("style") if thumbnail != "" { if matches := regexp.MustCompile(`url\((.*?)\)`).FindStringSubmatch(thumbnail); len(matches) > 1 { thumbnail = strings.Trim(matches[1], `"`) } } if title == "" || href == "" { return } trackURL, err := url.Parse(href) if err != nil { return } if trackURL.Host == "" { trackURL.Scheme = "https" trackURL.Host = "soundcloud.com" } trackURL.Path = strings.ReplaceAll(trackURL.Path, "//", "/") fullURL := trackURL.String() results = append(results, MusicResult{ Title: title, Artist: artist, URL: fullURL, Thumbnail: thumbnail, Source: "SoundCloud", }) }) return results, nil } func extractClientID() (string, error) { resp, err := http.Get("https://soundcloud.com/") if err != nil { return "", err } defer resp.Body.Close() doc, err := goquery.NewDocumentFromReader(resp.Body) if err != nil { return "", err } var clientID string doc.Find("script[src]").Each(func(i int, s *goquery.Selection) { if clientID != "" { return } src, _ := s.Attr("src") if strings.Contains(src, "sndcdn.com/assets/") { resp, err := http.Get(src) if err != nil { return } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) re := regexp.MustCompile(`client_id:"([^"]+)"`) matches := re.FindSubmatch(body) if len(matches) > 1 { clientID = string(matches[1]) } } }) if clientID == "" { return "", fmt.Errorf("client_id not found") } return clientID, nil }