198 lines
4.5 KiB
Go
198 lines
4.5 KiB
Go
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
|
|
}
|