parent
1acd1c0cab
commit
7cd5e80468
23 changed files with 988 additions and 5 deletions
198
music-soundcloud.go
Normal file
198
music-soundcloud.go
Normal file
|
@ -0,0 +1,198 @@
|
|||
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue